summaryrefslogtreecommitdiff
path: root/src/regexp.zig
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-29 13:43:52 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:16 -0300
commita945c035ce9e3d5fda81a05f95f3ea1788a19ece (patch)
tree54819b03526babd7c184051e104ef598c2143cb3 /src/regexp.zig
parent066f25a8cee7f5c63e991a9857667c346c6b75df (diff)
downloadpardes-a945c035ce9e3d5fda81a05f95f3ea1788a19ece.tar.gz
pardes-a945c035ce9e3d5fda81a05f95f3ea1788a19ece.zip
A ^ after | anchors its branch: ^def|^ finds lines starting either way
mvzr takes ^ only at its pattern's start, so in ^def|^ the second branch never matched mid-text. An alternation whose every top-level branch starts with ^ is now compiled as ^(def| ); a mix of anchored and unanchored branches is refused rather than silently wrong. Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src/regexp.zig')
-rw-r--r--src/regexp.zig65
1 files changed, 65 insertions, 0 deletions
diff --git a/src/regexp.zig b/src/regexp.zig
index 27b3c0ba..ca396ff1 100644
--- a/src/regexp.zig
+++ b/src/regexp.zig
@@ -48,6 +48,11 @@ pub const Regex = struct {
/// ends of the whole text and so never match where sam would.
pub fn compile(pat: []const u8) error{ Bad, Anchor }!Regex {
if (pat.len == 0) return error.Bad;
+ // mvzr takes `^` only at its pattern's start, so `^def|^ ` (a `^`
+ // after a `|`) is written `^(def| )` for it: the same lines. A mix,
+ // `^a|b`, has no such spelling and is refused rather than wrong.
+ var joined: [258]u8 = undefined;
+ if (try anchoredAlternation(pat, &joined)) |whole| return compile(whole);
var buf: [256]u8 = undefined;
var len: usize = 0;
var spans = false;
@@ -97,6 +102,56 @@ pub const Regex = struct {
};
}
+ /// `^a|^b` as `^(a|b)` in `out`, when the pattern is an alternation at
+ /// its top level and every branch starts with `^`; null when it is not
+ /// one, or no branch does.
+ fn anchoredAlternation(pat: []const u8, out: *[258]u8) error{Bad}!?[]const u8 {
+ var bars: [16]usize = undefined;
+ var n: usize = 0;
+ var depth: usize = 0;
+ var in_class = false;
+ var i: usize = 0;
+ while (i < pat.len) : (i += 1) {
+ const c = pat[i];
+ if (c == '\\') {
+ i += 1;
+ } else if (in_class) {
+ in_class = c != ']';
+ } else if (c == '[') {
+ in_class = true;
+ } else if (c == '(') {
+ depth += 1;
+ } else if (c == ')') {
+ depth -|= 1;
+ } else if (c == '|' and depth == 0) {
+ if (n == bars.len) return error.Bad;
+ bars[n] = i;
+ n += 1;
+ }
+ }
+ if (n == 0) return null;
+ var anchored: usize = 0;
+ var from: usize = 0;
+ for (0..n + 1) |k| {
+ const to = if (k < n) bars[k] else pat.len;
+ anchored += @intFromBool(to > from and pat[from] == '^');
+ from = to + 1;
+ }
+ if (anchored == 0) return null;
+ if (anchored != n + 1) return error.Bad;
+ var w = std.Io.Writer.fixed(out);
+ w.writeAll("^(") catch return error.Bad;
+ from = 0;
+ for (0..n + 1) |k| {
+ const to = if (k < n) bars[k] else pat.len;
+ if (k > 0) w.writeByte('|') catch return error.Bad;
+ w.writeAll(pat[from + 1 .. to]) catch return error.Bad;
+ from = to + 1;
+ }
+ w.writeByte(')') catch return error.Bad;
+ return w.buffered();
+ }
+
pub const Match = struct { start: usize, end: usize };
/// The first match that starts in `from..=last` and ends by `hi`, as
@@ -185,6 +240,16 @@ test "lines are haystacks: ^ and $ at each line, . never a newline, \\n spans li
try std.testing.expectError(error.Bad, Regex.compile("a\\"));
}
+test "a ^ after | anchors that branch: ^def|^ finds a line that starts either way, and a mix is refused" {
+ const text = "x def\n a\ndef b\n";
+ var both = try Regex.compile("^def|^ ");
+ try std.testing.expectEqual(@as(usize, 6), (try both.find(text, 0, text.len, text.len)).?.start);
+ try std.testing.expectEqual(@as(usize, 9), (try both.find(text, 7, text.len, text.len)).?.start);
+ try std.testing.expectError(error.Bad, Regex.compile("^def|x"));
+ var plain = try Regex.compile("a|b");
+ try std.testing.expectEqual(@as(usize, 7), (try plain.find(text, 0, text.len, text.len)).?.start);
+}
+
test "a quoted backslash before n is no newline, and a class ends where mvzr ends it" {
// `\\n` is a backslash then an n: the pattern stays on one line, so its
// `.` is not made [^\n] and does match within the line.