Forgive me if this is obviously dumb, but would it be possible to have a iterable captures form.
I’m thinking about something that didn’t need to be passed a buffer as it just returned slices of the original haystack. Each time you iterate it would return the next capture slice, or null if there is no next capture (i.e. regexp completed), or error if the regexp fails somewhere.
mvzr doesn’t have captures, but it has a RegexIterator which iterates matches. Proof of concept at least.
It doesn’t have captures primarily because it doesn’t allocate, but I see no reason why an iterator over (allocated) captures would need to allocate more.
Internally, captures are kept in a []u32, each pair is built into a struct { start: usize, end: usize } and copied to supplied buffer. This means that an CapturesIterator can simply build and return 1 pair at a time. Captures are fully populated by the time the engine finds a match.
Now the main problem with that is constant access to captures. For example, consider this pattern:
typically with captures you want to take the 4 digits in capture 3 (year) and map it directly to your data where you want it. With an iterator you can’t do that conveniently.
An Iterator fits much better when you want to iterate matches, which is what I’m planning to do.
That’s a good point. Hypothetically I can store []{u32,u32} pairs instead of []32 internally and return a slice of the internal storage to user. That’s something I will need to explore!
If I understand what you correctly, you’re saying that because the user doesn’t have access to the captures out of order / all at the same time - that makes using them difficult. …or are you thinking more about the ownership of the data, because the first thing I’m doing is probably converting to a more suitable type (e.g. year string into an integer).
Agree that an iterator form is only useful in a smaller set of circumstances, but I was trying to avoid needing a indeterminate sized buffer for storing the captures.
Yup! It was ultimately a bad design from my part, and @mnemnion pointed it out earlier and gave me a hint.
I just pushed an update that does not require the caller to supply a buffer just to get a match. The downside is that returned capture data becomes invalid after the next search on the same Regex, so I added a method Captures.copy(dest) so caller can save capture data if they wish.
know if the pattern matched (one method, returning bool)
get iterator over all matches, along with slice of their captures, so that I can index quickly (second method)
I can’t remember if I’ve ever wanted to get all matches along with their captures. I wouldn’t personally merge the api into one find(), but it’s just my opinion of course
Typically in circumstances like this I would create two APIs - (1) a non-allocating iterator for speed and (2) an allocating “return all captures” version that is a convenience around #1. The allocating version is important for adoption, otherwise casual users just move on.
What if there also was a way to specify a regex at comptime, for example via var re = Regex.comptime("(\\d\\d)/(\\d\\d)/(\\d\\d\\d\\d)", .{})? (similar to comptimePrint)
That could analyze the regex and count the number of possible capture groups and then generate a type that can hold all the meta information for a single match with multiple captures. (basically putting everything that would need to be allocated for the runtime version into the type on the stack, which I imagine would be a bunch of slices that point into parts of the searched string)
I think something like this could be useful in cases where the regex is comptime known, that said I think the general dynamic runtime case is much more important, just because it always works.
That’s a cool idea. I like that it’s not trying to replace the runtime case or anything just giving it a nicer path when the regex is known at comptime. Having the capture shape and storage fixed up front feels very Zig lol
It’s also why the current limitations of mvzr are liable to remain in place. I haven’t figured out a really good way to do captures without any allocation at all, or implement Unicode within a reasonable slice of stack.
The LPEG-like VM which backs it is also backtracks, which is hardly a problem for one kind of use, but precludes another: any situation where a bad actor is able to provide a regex to the engine. If the patterns are all developer-written, it’s a small “know what you’re doing” caveat.
On the other hand, we’re sure to get comptime allocation back one of these days.
Just pushed the update to support finding multiple matches from the same haystack. Here’s an example:
const std = @import("std");
const Regex = @import("regex");
pub fn main() !void {
const gpa = std.heap.page_allocator;
// Iterate over all non-overlapping matches with `findAll()`:
{
const haystack = "Hello World, Alice and Bob";
var re = try Regex.compile(gpa, "[A-Z][a-z]+", .{});
defer re.deinit();
var iter = re.findAll(haystack);
while (iter.next()) |m| {
std.debug.print("{s} [{}, {})\n", .{ m.bytes(haystack), m.start, m.end });
}
}
// Iterate over all matches with capture groups using `findAllCaptures()`:
// Each `Captures` yielded by `findAllCaptures()` is invalidated by the next
// `next()` call on that iterator. Use `Captures.copy(dest)` if you need to keep
// capture data after advancing.
{
const haystack = "x=12 y=34";
var re = try Regex.compile(gpa, "(?<key>\\w+)=(?<value>\\d+)", .{});
defer re.deinit();
var iter = re.findAllCaptures(haystack);
while (iter.next()) |caps| {
std.debug.print("{s} -> {s}\n", .{
caps.name("key").?.bytes(haystack),
caps.name("value").?.bytes(haystack),
});
}
}
}
@neurocyte I will now start working on unicode support. Once that gets to a reasonable stage, I would love to be the one to integrate this library into your editor project, if you’re ok with that! Because it will great for me to use this on a real consumer project, and an editor is the kind of cool thing that I wish I was working on. No pressure either way!
Initial UTF-8 support has been added! Because the engine operates on bytes, simple UTF-8 literals could already match as their raw byte sequence. But the Unicode support adds:
A u flag is now supported for Unicode mode.
In general, invalid UTF-8 in patterns is rejected with a parse error.
In u mode,
. matches one valid UTF-8 scalar instead of one raw byte,
bracket classes/ranges like [α-ω] compile as Unicode scalar ranges before being lowered to UTF-8 byte automata,
\x{...} can compile to a Unicode scalar matcher,
Those scalar matchers are still lowered to UTF-8 byte automata, so the VM continues to execute byte transitions.
With both i and u, case-insensitive matching now uses Unicode simple case folding from UCD 17.0.0.
Unicode properties like \p{Greek} and \pL are still planned, not implemented yet.
Docs for the current Unicode behavior and limitations are in docs/unicode.md.
Implementing UTF-8 support required a lot of research/reading and the project did not progress for a while, so I got bored and did some optimization testing trying to find some small win, and got one!
The parser now stores concat/alternation child indices in one shared side pool instead of giving each AST node its own slice. Character class items moved to the same kind of side-pool layout. Together, those changes make the largest AST node a lot smaller (from 40 bits to 16 bits). It also enabled a nice pattern where parser allocations are reused and total allocation count drops significantly. In local rough benchmarks, this improved parse and parse+compile time on a small pattern set by about 30% (!!).
This approach is what I read from the Zig compiler/saw from Andrew’s video and was inspired to test for a while, but was still surprised at how effective it is! I’m quite happy to report a successful use of it here.