Skip to content

Commit 7a6d83e

Browse files
committed
fix(regex): fast-path findFrom repeated atoms
Mirror the repeated byte-atom and Unicode-property atom fast paths in Regex.findFrom when the caller starts at a nonzero byte offset. This keeps global/sticky JS RegExp searches from falling back to the per-position VM path after lastIndex advances, while preserving anchored-start behavior. Test262 case flips: 0 observed pass/fail flips. Verified zig-regex src/regex.zig tests 99/99, rebuilt zig-js test262-bin, and checked RegExp/prototype/exec 79/79, RegExp/prototype/test 45/45, and generated property shard #0:2 2/2.
1 parent e4c5a94 commit 7a6d83e

1 file changed

Lines changed: 87 additions & 6 deletions

File tree

src/regex.zig

Lines changed: 87 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -960,6 +960,10 @@ pub const Regex = struct {
960960
return .{ .count = run, .end = p };
961961
}
962962

963+
fn anchoredStartSkipped(self: *const Regex, start: usize) bool {
964+
return start > 0 and self.opt_info.anchored_start and !self.flags.multiline;
965+
}
966+
963967
fn nextCodepointStart(input: []const u8, pos: usize) usize {
964968
if (pos >= input.len) return input.len + 1;
965969
const len = common.ecmaCodePointLen(input, pos) orelse return pos + 1;
@@ -1470,15 +1474,66 @@ pub const Regex = struct {
14701474
pub fn findFrom(self: *const Regex, input: []const u8, start: usize) !?Match {
14711475
if (start == 0) return self.find(input);
14721476
if (self.requiredAbsent(input)) return null;
1477+
const from = @min(start, input.len);
1478+
if (self.anchoredStartSkipped(from)) return null;
14731479

14741480
if (self.opt_info.exact_literal) |lit| {
14751481
const found = if (self.flags.case_insensitive) blk: {
1476-
var sc = CiLiteralScanner.init(input, lit, @min(start, input.len));
1482+
var sc = CiLiteralScanner.init(input, lit, from);
14771483
break :blk sc.next();
1478-
} else literalSearch(input, lit, @min(start, input.len));
1484+
} else literalSearch(input, lit, from);
14791485
if (found) |i| return Match{ .slice = input[i .. i + lit.len], .start = i, .end = i + lit.len };
14801486
return null;
14811487
}
1488+
if (self.engine_type == .thompson_nfa) if (self.opt_info.repeat_atom) |ra| if (self.repeatAtomFastPathOk()) {
1489+
const table = repeatTable(ra, self.flags.case_insensitive);
1490+
const max = ra.max orelse std.math.maxInt(usize);
1491+
if (self.opt_info.anchored_end) {
1492+
var p = from;
1493+
while (p < input.len and !table[input[p]]) p += 1;
1494+
if (p >= input.len) return null;
1495+
const run = repeatRunAt(input, table, p, max);
1496+
const end = p + run;
1497+
if (end == input.len and repeatBoundsOk(run, ra.min, max))
1498+
return Match{ .slice = input[p..end], .start = p, .end = end };
1499+
return null;
1500+
}
1501+
var p = from;
1502+
while (p < input.len) {
1503+
while (p < input.len and !table[input[p]]) p += 1;
1504+
if (p >= input.len) break;
1505+
const match_start = p;
1506+
var run: usize = 0;
1507+
while (p < input.len and table[input[p]] and run < max) : (p += 1) run += 1;
1508+
if (run >= ra.min) return Match{ .slice = input[match_start..p], .start = match_start, .end = p };
1509+
}
1510+
return null;
1511+
};
1512+
if (self.opt_info.unicode_repeat_atom) |ura| if (!self.flags.case_insensitive and self.repeatAtomFastPathOk()) {
1513+
const max = ura.max orelse std.math.maxInt(usize);
1514+
if (self.opt_info.anchored_end) {
1515+
var p = from;
1516+
while (p < input.len) {
1517+
const run = unicodeRepeatRunAt(input, ura.property, p, max);
1518+
if (run.count >= ura.min) {
1519+
if (run.end == input.len)
1520+
return Match{ .slice = input[p..run.end], .start = p, .end = run.end };
1521+
return null;
1522+
}
1523+
p = nextCodepointStart(input, p);
1524+
}
1525+
return null;
1526+
}
1527+
var p = from;
1528+
while (p < input.len) {
1529+
const run = unicodeRepeatRunAt(input, ura.property, p, max);
1530+
if (run.count >= ura.min) {
1531+
return Match{ .slice = input[p..run.end], .start = p, .end = run.end };
1532+
}
1533+
p = nextCodepointStart(input, p);
1534+
}
1535+
return null;
1536+
};
14821537

14831538
switch (self.engine_type) {
14841539
.thompson_nfa => {
@@ -1488,7 +1543,7 @@ pub const Regex = struct {
14881543

14891544
if (self.onepass) |plan| {
14901545
const fb = self.opt_info.first_bytes;
1491-
var scan: usize = @min(start, input.len);
1546+
var scan: usize = from;
14921547
while (scan <= input.len) {
14931548
if (fb) |t| {
14941549
while (scan < input.len and !t[input[scan]]) scan += 1;
@@ -1504,7 +1559,7 @@ pub const Regex = struct {
15041559

15051560
if (self.opt_info.literal_prefix) |prefix| {
15061561
if (!self.flags.case_insensitive) {
1507-
var search_from: usize = @min(start, input.len);
1562+
var search_from: usize = from;
15081563
while (std.mem.indexOf(u8, input[search_from..], prefix)) |rel_pos| {
15091564
const prefix_pos = search_from + rel_pos;
15101565
if (try virtual_machine.matchAt(input, prefix_pos)) |result| {
@@ -1518,7 +1573,7 @@ pub const Regex = struct {
15181573

15191574
if (self.opt_info.first_bytes) |fb| {
15201575
if (!self.flags.case_insensitive) {
1521-
var scan: usize = @min(start, input.len);
1576+
var scan: usize = from;
15221577
while (scan < input.len) {
15231578
while (scan < input.len and !fb[input[scan]]) scan += 1;
15241579
if (scan >= input.len) break;
@@ -1531,7 +1586,7 @@ pub const Regex = struct {
15311586
}
15321587
}
15331588

1534-
var scan: usize = @min(start, input.len);
1589+
var scan: usize = from;
15351590
while (scan <= input.len) {
15361591
if (try virtual_machine.matchAt(input, scan)) |result| {
15371592
return try self.buildMatch(input, result);
@@ -3057,6 +3112,32 @@ test "unicode surrogate escapes compile to WTF-8 code units" {
30573112
try std.testing.expect(try regex.isMatch(&.{ 0xED, 0xBC, 0x86 }));
30583113
}
30593114

3115+
test "findFrom uses repeated atom fast paths after nonzero start" {
3116+
const allocator = std.testing.allocator;
3117+
3118+
var digits = try Regex.compile(allocator, "\\d+");
3119+
defer digits.deinit();
3120+
if (try digits.findFrom("xx123yy", 2)) |match_result| {
3121+
try std.testing.expectEqualStrings("123", match_result.slice);
3122+
} else try std.testing.expect(false);
3123+
if (try digits.findFrom("xx123yy", 4)) |match_result| {
3124+
try std.testing.expectEqualStrings("3", match_result.slice);
3125+
} else try std.testing.expect(false);
3126+
3127+
var letters = try Regex.compileWithFlags(allocator, "\\p{Letter}+", .{ .unicode = true });
3128+
defer letters.deinit();
3129+
if (try letters.findFrom("12abc", 2)) |match_result| {
3130+
try std.testing.expectEqualStrings("abc", match_result.slice);
3131+
} else try std.testing.expect(false);
3132+
if (try letters.findFrom("12abc", 3)) |match_result| {
3133+
try std.testing.expectEqualStrings("bc", match_result.slice);
3134+
} else try std.testing.expect(false);
3135+
3136+
var anchored = try Regex.compileWithFlags(allocator, "^\\p{Letter}+$", .{ .unicode = true });
3137+
defer anchored.deinit();
3138+
try std.testing.expect((try anchored.findFrom("abc", 1)) == null);
3139+
}
3140+
30603141
test "capture spans give per-group byte offsets" {
30613142
const allocator = std.testing.allocator;
30623143
var regex = try Regex.compile(allocator, "(\\d+)-(\\d+)");

0 commit comments

Comments
 (0)