summaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGabriel Schneider <[email protected]>2026-09-30 11:50:52 -0300
committerGabriel Schneider <[email protected]>2026-10-01 00:12:17 -0300
commit7c329baddb0d45ce7937cf29941dde861f9ea27f (patch)
tree76919df40dbaccf6cd148dcbf2f46efc508df76c /src
parentfc8e1abf361ab2151109e68e5c88a0ed45650b38 (diff)
downloadpardes-7c329baddb0d45ce7937cf29941dde861f9ea27f.tar.gz
pardes-7c329baddb0d45ce7937cf29941dde861f9ea27f.zip
A pattern anchoring some alternatives with ^ and not others is refused with why, not a bare bad regular expression
`^def|x` and `foo$|^zèbre` were refused with a bare "bad regular expression", the same words as a syntax error, so a script could not tell what to change. They now say "an alternation anchors every branch with ^ or none (^a|^b, not ^a|b)", through addr and Edit alike. fs.md states the rule and that a `$` does not count toward it (`foo$|bar` is fine). Co-Authored-By: Claude Opus 5.5 <[email protected]>
Diffstat (limited to 'src')
-rw-r--r--src/ninep/addr.zig13
-rw-r--r--src/regexp.zig10
-rw-r--r--src/sam_edit.zig1
3 files changed, 20 insertions, 4 deletions
diff --git a/src/ninep/addr.zig b/src/ninep/addr.zig
index 33a3f646..9674a323 100644
--- a/src/ninep/addr.zig
+++ b/src/ninep/addr.zig
@@ -304,6 +304,7 @@ pub const Addr = struct {
error.TooLong => regexp_.Regex.e_long,
error.WideRange => regexp_.Regex.e_wide,
error.NegatedRunes => regexp_.Regex.e_negated,
+ error.Mixed => regexp_.Regex.e_mixed,
error.Bad => e_regexp,
};
return null;
@@ -383,6 +384,18 @@ test "an address is evaluated from the current one, as acme's are" {
try testing.expectEqualStrings(e_order, th.wr(q, qaddr, "/a/,/b/").reply.ename);
}
+test "an alternation anchoring some branches and not others is refused with why, a $ not counting" {
+ const p = try th.withFile(testing.allocator, "def\nfoo\n");
+ defer p.deinit();
+ const node = Node.of(th.serialOf(p), .addr);
+ for ([_][]const u8{ "/^def|x/", "/foo$|^zèbre/" }) |pat| {
+ const r = th.wr(p, node, pat);
+ try testing.expectEqual(E.INVAL, r.errno());
+ try testing.expectEqualStrings(regexp_.Regex.e_mixed, r.reply.ename);
+ }
+ try testing.expectEqual(Status.ok, th.wr(p, node, "/foo$|def/").reply.status);
+}
+
test "a pattern with a newline and an anchor mvzr would misread is refused EINVAL, by addr and by Edit" {
const p = try th.withFile(testing.allocator, "a\nfoo\n");
defer p.deinit();
diff --git a/src/regexp.zig b/src/regexp.zig
index ddb5eb28..575e6861 100644
--- a/src/regexp.zig
+++ b/src/regexp.zig
@@ -56,6 +56,7 @@ pub const Regex = struct {
pub const e_anchor = "bad regular expression: in a pattern with \\n, ^ can only come first and $ only just before a \\n";
pub const e_wide = std.fmt.comptimePrint("bad regular expression: a range of runes wider than {d} in [...] is not supported", .{max_range});
pub const e_negated = "bad regular expression: a [^...] with non-ASCII runes is not supported (mvzr's classes hold bytes)";
+ pub const e_mixed = "bad regular expression: an alternation anchors every branch with ^ or none (^a|^b, not ^a|b)";
/// The most runes a non-ASCII range in a class is spelled out as.
pub const max_range = 256;
@@ -79,7 +80,7 @@ pub const Regex = struct {
return @intCast(n);
}
- pub const Error = error{ Bad, Anchor, TooLong, WideRange, NegatedRunes };
+ pub const Error = error{ Bad, Anchor, TooLong, WideRange, NegatedRunes, Mixed };
pub fn compile(pat: []const u8) Error!Regex {
if (pat.len == 0) return error.Bad;
@@ -273,7 +274,7 @@ 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: *[5 * max_ops + 2]u8) error{Bad}!?[]const u8 {
+ fn anchoredAlternation(pat: []const u8, out: *[5 * max_ops + 2]u8) error{ Bad, Mixed }!?[]const u8 {
var bars: [16]usize = undefined;
var n: usize = 0;
var depth: usize = 0;
@@ -306,7 +307,8 @@ pub const Regex = struct {
from = to + 1;
}
if (anchored == 0) return null;
- if (anchored != n + 1) return error.Bad;
+ // Only `^` counts: `$` is mvzr's own at a branch's end (`a$|b`).
+ if (anchored != n + 1) return error.Mixed;
var w = std.Io.Writer.fixed(out);
w.writeAll("^(") catch return error.Bad;
from = 0;
@@ -481,7 +483,7 @@ test "a ^ after | anchors that branch: ^def|^ finds a line that starts either w
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"));
+ try std.testing.expectError(error.Mixed, 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);
}
diff --git a/src/sam_edit.zig b/src/sam_edit.zig
index 0f317a83..94cf6fe0 100644
--- a/src/sam_edit.zig
+++ b/src/sam_edit.zig
@@ -463,6 +463,7 @@ const Exec = struct {
error.TooLong => fail(ex.why, "{s}", .{regexp.Regex.e_long}),
error.WideRange => fail(ex.why, "{s}", .{regexp.Regex.e_wide}),
error.NegatedRunes => fail(ex.why, "{s}", .{regexp.Regex.e_negated}),
+ error.Mixed => fail(ex.why, "{s}", .{regexp.Regex.e_mixed}),
error.Bad => fail(ex.why, "bad regexp in {c} command", .{c}),
};
}