// Copyright 2022-2024 Herb Sutter // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception // // Part of the Cppfront Project, under the Apache License v2.0 with LLVM Exceptions. // See https://github.com/hsutter/cppfront/blob/main/LICENSE for license information. //=========================================================================== // Regex support //=========================================================================== #ifndef CPP2_CPP2REGEX_H #define CPP2_CPP2REGEX_H template using matcher_wrapper_type = typename matcher_wrapper::template wrap; template using matcher_context_type = typename matcher::context; cpp2: namespace = { regex: namespace = { bstring: type == std::basic_string; bview : type == std::basic_string_view; //----------------------------------------------------------------------- // // Helper structures for the expression matching. // //----------------------------------------------------------------------- // // Structure for storing group information. // match_group: @struct type = { start: Iter = (); end: Iter = (); matched: bool = false; } // Return value for every matcher. // match_return: @struct type = { matched: bool = false; pos: Iter = (); } // Modifiable state during matching. // match_context: type = { public begin : Iter; public end : Iter; private groups: std::array, max_groups> = (); operator=: (out this, begin_: Iter, end_: Iter) = { begin = begin_; end = end_; } operator=: (out this, that) = {} // Getter and setter for groups // get_group: (in this, group) = groups[group]; get_group_end: (in this, group) -> int = { if group >= max_groups || !groups[group].matched { return 0; } return cpp2::unchecked_narrow( std::distance(begin, groups[group].end) ); } get_group_start: (in this, group) -> int = { if group >= max_groups || !groups[group].matched { return 0; } return cpp2::unchecked_narrow( std::distance(begin, groups[group].start) ); } get_group_string: (in this, group) -> std::string = { if group >= max_groups || !groups[group].matched { return ""; } return std::string(groups[group].start, groups[group].end); } set_group_end: (inout this, group, pos) = { groups[group].end = pos; groups[group].matched = true; } set_group_invalid: (inout this, group) = { groups[group].matched = false; } set_group_start: (inout this, group, pos) = { groups[group].start = pos; } size: (in this) = max_groups; // Misc functions // fail: (in this) = match_return(false, end); pass: (in this, cur: Iter) = match_return(true, cur); } // End function that returns a valid match. // true_end_func: @struct type = { operator(): (in this, cur, inout ctx) = ctx..pass(cur); } // Empty group reset function. // no_reset: @struct type = { operator(): (this, inout _:) = {} } // Evaluate func on destruction of the handle. on_return: type = { func: Func; operator=: (out this, f: Func) = { func = f; } operator=: (move this) = { func(); } } // Helper for auto deduction of the Func type. make_on_return: (func: Func) = on_return(func); //----------------------------------------------------------------------- // // Character classes for regular expressions. // //----------------------------------------------------------------------- // // Class syntax: Example: a // single_class_entry: type = { includes : (c: CharT) = c == C; to_string: () = bstring(1, C); } // Class syntax: - Example: a-c // range_class_entry: type = { includes : (c: CharT) = Start <= c <= End; to_string: () = "(Start)$-(End)$"; } // Helper for combining two character classes // combined_class_entry: type = { includes : (c: CharT) = (false || ... || List::includes(c)); to_string: () = (bstring() + ... + List::to_string()); } // Class syntax: Example: abcd // list_class_entry: type = { includes : (c: CharT) = (false || ... || (List == c)); to_string: () = (bstring() + ... + List); } // Class syntax: [: type = { includes : (c: CharT) = Inner::includes(c); to_string: () = "[:(Name..data())$:]"; } negated_class_entry: type = { this : Inner = (); includes: (c: CharT) = !Inner::includes(c); } // Short class syntax: \ Example: \w // shorthand_class_entry: type = { includes : (c: CharT) = Inner::includes(c); to_string: () = Name..str(); } // Named basic character classes // digits_class : type == named_class_entry>; lower_class : type == named_class_entry>; upper_class : type == named_class_entry>; // Named other classes // alnum_class : type == named_class_entry, upper_class, digits_class>>; alpha_class : type == named_class_entry, upper_class>>; ascii_class : type == named_class_entry>; blank_class : type == named_class_entry>; cntrl_class : type == named_class_entry, single_class_entry>>; graph_class : type == named_class_entry>; hor_space_class : type == named_class_entry>; print_class : type == named_class_entry>; punct_class : type == named_class_entry','?','@','[','\\',']','^','_','`','{','|','}','~',']'>>; space_class : type == named_class_entry>; ver_space_class : type == named_class_entry>; word_class : type == named_class_entry, single_class_entry>>; xdigit_class : type == named_class_entry, range_class_entry, digits_class>>; // Shorthand class entries // short_digits_class : type == shorthand_class_entry>; short_hor_space_class : type == shorthand_class_entry>; short_space_class : type == shorthand_class_entry>; short_vert_space_class : type == shorthand_class_entry>; short_word_class : type == shorthand_class_entry>; short_not_digits_class : type == negated_class_entry>>; short_not_hor_space_class : type == negated_class_entry>>; short_not_space_class : type == negated_class_entry>>; short_not_vert_space_class : type == negated_class_entry>>; short_not_word_class : type == negated_class_entry>>; // Regex syntax: | Example: ab|ba // // Non greedy implementation. First alternative that matches is chosen. // alternative_token_matcher: type = { match: (cur, inout ctx, end_func, tail, functions ...) -> _ = { return match_first(cur, ctx, end_func, tail, functions...); } private match_first: (cur, inout ctx, end_func, tail, cur_func, cur_reset, other ...: Other) -> _ = { inner_call := :(tail_cur, inout tail_ctx) -> _ == { return (tail)$(tail_cur, tail_ctx, (end_func)$); }; r := cur_func(cur, ctx, inner_call); if r.matched { return r; } else { cur_reset(ctx); if constexpr 0 != sizeof...(Other) { return match_first(cur, ctx, end_func, tail, other...); } else { return ctx..fail(); } } } } // Regex syntax: . // any_token_matcher: (inout cur, inout ctx) -> bool = { if cur != ctx.end // any char except the end && (single_line || cur* != '\n') // do not match new lines in multi line mode { cur += 1; return true; } // Else return false; } // TODO: Check if vectorization works at some point with this implementation. // char_token_matcher: (inout cur, inout ctx) -> bool = { // if !(std::distance(cur, ctx.end) < tokens..size()) { // return false; // } // matched : bool = true; // (copy i: int = 0) while i < tokens..size() next i += 1 { // if tokens..data()[i] != cur[i] { // matched = false; // No break for performance optimization. Without break, the loop vectorizes. // } // } // if matched { // cur += tokens..size(); // } // return matched; // } // char_token_case_insensitive_matcher: (inout cur, inout ctx) -> bool = { // if !(std::distance(cur, ctx.end) < lower..size()) { // return false; // } // matched : bool = true; // (copy i : int = 0) while i < lower..size() next i += 1 { // if !(lower..data()[i] == cur[i] || upper..data()[i] == cur[i]) { // matched = false; // No break for performance optimization. Without break, the loop vectorizes. // } // } // if matched { // cur += lower..size(); // } // return matched; // } // Regex syntax: [] Example: [abcx-y[:digits:]] // class_token_matcher: type = { match: (inout cur, inout ctx) -> bool = { if constexpr case_insensitive { if cur != ctx.end && negate != ( match_any(string_util::safe_tolower(cur*)) || match_any(string_util::safe_toupper(cur*)) ) { cur += 1; return true; } else { return false; } } else { if cur != ctx.end && negate != match_any(cur*) { cur += 1; return true; } else { return false; } } } private match_any: (c: CharT) -> bool = { r: bool = First::includes(c); if !r { if constexpr 0 != sizeof...(Other) { r = match_any(c); } } return r; } // TODO: Implement proper to string // to_string: () -> bstring = { // r: bstring = "["; // if negate { // r += "^"; // } // r += (bstring() + ... + List::to_string()); // r += "]"; // return r; // } } // Named short classes // named_class_no_new_line : type == class_token_matcher>; named_class_digits : type == class_token_matcher>; named_class_hor_space : type == class_token_matcher>; named_class_space : type == class_token_matcher>; named_class_ver_space : type == class_token_matcher>; named_class_word : type == class_token_matcher>; named_class_not_digits : type == class_token_matcher>; named_class_not_hor_space : type == class_token_matcher>; named_class_not_space : type == class_token_matcher>; named_class_not_ver_space : type == class_token_matcher>; named_class_not_word : type == class_token_matcher>; // Regex syntax: \ Example: \1 // \g{name_or_number} // \k{name_or_number} // \k // \k'name_or_number' // group_ref_token_matcher: (inout cur, inout ctx) -> bool = { g := ctx..get_group(group); group_pos := g.start; while group_pos != g.end && cur != ctx.end next (group_pos++, cur++) { if constexpr case_insensitive { if string_util::safe_tolower(group_pos*) != string_util::safe_tolower(cur*) { return false; } } else { if group_pos* != cur* { return false; } } } if group_pos == g.end { return true; } else { return false; } } // Regex syntax: $ Example: aa$ // line_end_token_matcher: (cur, inout ctx) -> bool = { if cur == ctx.end || (match_new_line && cur* == '\n') { return true; } else if match_new_line_before_end && (cur* == '\n' && (cur + 1) == ctx.end) { // Special case for new line at end. return true; } else { return false; } } // Regex syntax: ^ Example: ^aa // line_start_token_matcher: (cur, inout ctx) -> bool = { return cur == ctx.begin || // Start of string (match_new_line && (cur - 1)* == '\n'); // Start of new line } // Regex syntax: (?=) or (?!) or (*pla), etc. Example: (?=AA) // // Parsed in group_token. // lookahead_token_matcher: (cur, inout ctx, func) -> bool = { r := func(cur, ctx, true_end_func()); if !positive { r.matched = !r.matched; } return r.matched; } // TODO: @enum as template parameter currently not working. See issue https://github.com/hsutter/cppfront/issues/1147 // Options for range matching. range_flags: type = { not_greedy: int == 1; // Try to take as few as possible. greedy: int == 2; // Try to take as many as possible. possessive: int == 3; // Do not give back after a greedy match. No backtracking. } // Regex syntax: {min, max} Example: a{2,4} // range_token_matcher: type = { match: (cur: Iter, inout ctx, inner, reset_func, end_func, tail) -> _ = { if range_flags::possessive == kind { return match_possessive(cur, ctx, inner, end_func, tail); } else if range_flags::greedy == kind { return match_greedy(0, cur, ctx.end, ctx, inner, reset_func, end_func, tail); } else { // range_flags::not_greedy == kind return match_not_greedy(cur, ctx, inner, end_func, tail); } } private is_below_upper_bound: (count: int) -> bool = { if -1 == max_count { return true; } else { return count < max_count; } } private is_below_lower_bound: (count: int) -> bool = { if -1 == min_count { return false; } else { return count < min_count; } } private is_in_range: (count: int) -> bool = { if -1 != min_count && count < min_count { return false; } if -1 != max_count && count > max_count { return false; } return true; } private match_min_count: (cur: Iter, inout ctx, inner, end_func, inout count_r: int) -> _ = { // TODO: count_r as out parameter introduces a performance loss. res := ctx..pass(cur); count := 0; while is_below_lower_bound(count) && res.matched { res = inner(res.pos, ctx, end_func); if res.matched { count += 1; } } count_r = count; return res; } private match_greedy: (count: int, cur: Iter, last_valid: Iter, inout ctx, inner, reset_func, end_func, other) -> match_return = { inner_call := :(tail_cur, inout tail_ctx) -> _ == { return match_greedy((count + 1)$, tail_cur, (cur)$, tail_ctx, (inner)$, (reset_func)$, (end_func)$, (other)$); }; is_m_valid := true; r := ctx..fail(); if is_below_upper_bound(count) && (is_below_lower_bound(count) || cur != last_valid) { is_m_valid = false; // Group ranges in M are invalidated through the call. r = inner(cur, ctx, inner_call); } if !r.matched && is_in_range(count) { // The recursion did not yield a match try now the tail r = other(cur, ctx, end_func); if r.matched && !is_m_valid{ // We have a match rematch M if required reset_func(ctx); if count > 0 { _ = inner(last_valid, ctx, true_end_func()); } } } return r; } private match_possessive: (cur: Iter, inout ctx, inner, end_func, other) -> match_return = { count :=0; r := match_min_count(cur, ctx, inner, end_func, count); if !r.matched { return r; } pos := r.pos; while r.matched && is_below_upper_bound(count) { r = inner(pos, ctx, true_end_func()); if pos == r.pos { break; // Break infinite loop. } if r.matched { count += 1; pos = r.pos; } } return other(pos, ctx, end_func); } private match_not_greedy: (cur: Iter, inout ctx, inner, end_func, other) -> match_return = { count := 0; start := match_min_count(cur, ctx, inner, end_func, count); if !start.matched { return start; } pos := start.pos; while is_below_upper_bound(count) { o:= other(pos, ctx, end_func); if o.matched { return o; } r:= inner(pos, ctx, end_func); if !r.matched { return ctx..fail(); } count += 1; pos = r.pos; } return other(pos, ctx, end_func); // Upper bound reached. } } // Regex syntax: \b or \B Example: \bword\b // // Matches the start end end of word boundaries. // word_boundary_token_matcher: (inout cur, inout ctx) -> bool = { words : word_class = (); is_match := false; if cur == ctx.begin { // String start if cur != ctx.end { // No empty string is_match = words..includes(cur*); } } else if cur == ctx.end { // String end is_match = words..includes((cur - 1)*); } else { // Middle of string is_match = (words..includes((cur - 1)*) && !words..includes(cur*)) // End of word: \w\W || (!words..includes((cur - 1)*) && words..includes(cur*)); // Start of word: \W\w } if negate { is_match = !is_match; } return is_match; } //----------------------------------------------------------------------- // // Regular expression implementation. // //----------------------------------------------------------------------- // // Regular expression implementation regular_expression: type = { matcher: type == matcher_wrapper_type; // TODO: Remove when nested types are allowed: https://github.com/hsutter/cppfront/issues/727 context: type == matcher_context_type>; // TODO: Remove when nested types are allowed: https://github.com/hsutter/cppfront/issues/727 search_return: type = { public matched: bool; public ctx: context; public pos: int; operator=:(out this, matched_: bool, ctx_: context, pos_: Iter) = { matched = matched_; ctx = ctx_; pos = unchecked_narrow(std::distance(ctx_.begin, pos_)); } group_number: (this) = ctx..size(); group: (this, g: int) = ctx..get_group_string(g); group_start: (this, g: int) = ctx..get_group_start(g); group_end: (this, g: int) = ctx..get_group_end(g); group: (this, g: bstring) = group(get_group_id(g)); group_start: (this, g: bstring) = group_start(get_group_id(g)); group_end: (this, g: bstring) = group_end(get_group_id(g)); private get_group_id: (this, g: bstring) -> _ = { group_id := matcher::get_named_group_index(g); if -1 == group_id { // TODO: Throw error. } return group_id; } } match: (in this, str: bview) = match(str..begin(), str..end()); match: (in this, str: bview, start) = match(get_iter(str, start), str..end()); match: (in this, str: bview, start, length) = match(get_iter(str, start), get_iter(str, start + length)); match: (in this, start: Iter, end: Iter) -> search_return = { ctx: context = (start, end); r := matcher::entry(start, ctx); return search_return(r.matched && r.pos == end, ctx, r.pos); } search: (in this, str: bview) = search(str..begin(), str..end()); search: (in this, str: bview, start) = search(get_iter(str, start), str..end()); search: (in this, str: bview, start, length) = search(get_iter(str, start), get_iter(str, start + length)); search: (in this, start: Iter, end: Iter) -> search_return = { ctx: context = (start, end); r := ctx..fail(); cur:= start; while true next (cur++) { r = matcher::entry(cur, ctx); if r.matched { break; } if cur == ctx.end { break; } } return search_return(r.matched, ctx, r.pos); } to_string: (in this) = matcher_wrapper::to_string(); // Helper functions // private get_iter: (str: bview, pos) -> _ = { if pos < str..size() { return str..begin() + pos; } else { return str..end(); } } } } } #endif