Skip to content

Commit 392df2e

Browse files
committed
test(regression): assert engine, not wall-clock, for fast-path guards
The `\w+\s+\w+` and `//.*` regression guards used a doubling-time ratio (t2/t1 < 3.0) to assert linearity, which is flaky under machine load. Replace them with a structural invariant — the pattern must compile to the `thompson_nfa` byte engine (it would be `.backtracking` if `\s`/`.` regressed off the fast path) — plus a large-input count for correctness. Same coverage, no timing flakiness.
1 parent 9cdd45b commit 392df2e

1 file changed

Lines changed: 25 additions & 82 deletions

File tree

‎tests/regression.zig‎

Lines changed: 25 additions & 82 deletions
Original file line numberDiff line numberDiff line change
@@ -533,54 +533,25 @@ test "regression: leading optional `\\s*` keeps the prefilter sound" {
533533
try std.testing.expectEqualStrings("c", parts[2]);
534534
}
535535

536-
test "regression: `\\w+\\s+\\w+` stays off the quadratic backtracker" {
536+
test "regression: `\\w+\\s+\\w+` stays on the byte engine (not the backtracker)" {
537537
const allocator = std.testing.allocator;
538538
var regex = try Regex.compile(allocator, "\\w+\\s+\\w+");
539539
defer regex.deinit();
540540

541-
// Build "wd wd wd ..." — dense matches; the old backtracking path was
542-
// ~O(n*m). Doubling the input must not more-than-triple the time.
543-
const make = struct {
544-
fn buf(a: std.mem.Allocator, words: usize) ![]u8 {
545-
var list = try std.ArrayList(u8).initCapacity(a, words * 3);
546-
errdefer list.deinit(a);
547-
var i: usize = 0;
548-
while (i < words) : (i += 1) {
549-
if (i != 0) try list.append(a, ' ');
550-
try list.appendSlice(a, "wd");
551-
}
552-
return list.toOwnedSlice(a);
553-
}
554-
};
555-
556-
const time = struct {
557-
fn run(re: *const Regex, b: []const u8) !u64 {
558-
_ = try re.count(b); // warm up
559-
var best: u64 = std.math.maxInt(u64);
560-
var rep: usize = 0;
561-
while (rep < 3) : (rep += 1) {
562-
const t0 = monotonicNs();
563-
const c = try re.count(b);
564-
const dt = monotonicNs() - t0;
565-
std.mem.doNotOptimizeAway(c);
566-
if (dt < best) best = dt;
567-
}
568-
return best;
569-
}
570-
};
571-
572-
const b1 = try make.buf(allocator, 20_000);
573-
defer allocator.free(b1);
574-
const b2 = try make.buf(allocator, 40_000); // exactly 2x
575-
defer allocator.free(b2);
541+
// The whole point of the `\s` lowering: this must run on the Thompson/DFA
542+
// byte engine, not collapse onto the O(n*m) backtracker the moment a `\s`
543+
// appears. Asserting the engine is a stable structural invariant (no flaky
544+
// wall-clock ratio), and a large dense input confirms it stays linear.
545+
try std.testing.expect(regex.engine_type == .thompson_nfa);
576546

577-
try std.testing.expectEqual(@as(usize, 10_000), try regex.count(b1));
578-
try std.testing.expectEqual(@as(usize, 20_000), try regex.count(b2));
579-
580-
const t1 = try time.run(&regex, b1);
581-
const t2 = try time.run(&regex, b2);
582-
try std.testing.expect(t1 > 0);
583-
try std.testing.expect(t2 * 10 < t1 * 30); // t2/t1 < 3.0 (linear, not quadratic)
547+
var list = try std.ArrayList(u8).initCapacity(allocator, 120_000);
548+
defer list.deinit(allocator);
549+
var i: usize = 0;
550+
while (i < 40_000) : (i += 1) {
551+
if (i != 0) try list.append(allocator, ' ');
552+
try list.appendSlice(allocator, "wd");
553+
}
554+
try std.testing.expectEqual(@as(usize, 20_000), try regex.count(list.items));
584555
}
585556

586557
test "regression: prefilter hints are exposed to callers (issue #10)" {
@@ -640,50 +611,22 @@ test "regression: `.*` stops at a newline; dot_all crosses it" {
640611
try std.testing.expectEqualStrings("axy\nz", m2.slice);
641612
}
642613

643-
test "regression: `//.*` line-comment scan stays off the backtracker" {
614+
test "regression: `//.*` stays on the byte engine (not the backtracker)" {
644615
const allocator = std.testing.allocator;
645616
var re = try Regex.compile(allocator, "//.*");
646617
defer re.deinit();
647618

648-
// Build "code //comment\n" repeated; doubling input must stay near-linear
649-
// (the old backtracking `.` path was the 44x-slower case in issue #10).
650-
const make = struct {
651-
fn buf(a: std.mem.Allocator, lines: usize) ![]u8 {
652-
var list = try std.ArrayList(u8).initCapacity(a, lines * 20);
653-
errdefer list.deinit(a);
654-
var i: usize = 0;
655-
while (i < lines) : (i += 1) try list.appendSlice(a, "x = 1; // note here\n");
656-
return list.toOwnedSlice(a);
657-
}
658-
};
659-
const time = struct {
660-
fn run(re_: *const Regex, b: []const u8) !u64 {
661-
_ = try re_.count(b);
662-
var best: u64 = std.math.maxInt(u64);
663-
var rep: usize = 0;
664-
while (rep < 3) : (rep += 1) {
665-
const t0 = monotonicNs();
666-
const c = try re_.count(b);
667-
const dt = monotonicNs() - t0;
668-
std.mem.doNotOptimizeAway(c);
669-
if (dt < best) best = dt;
670-
}
671-
return best;
672-
}
673-
};
674-
675-
const b1 = try make.buf(allocator, 20_000);
676-
defer allocator.free(b1);
677-
const b2 = try make.buf(allocator, 40_000); // 2x
678-
defer allocator.free(b2);
619+
// `.` used to force the backtracking engine (the 44x-slower case in issue
620+
// #10); it now lowers to a byte automaton. Assert the engine (a stable
621+
// structural invariant, not a flaky timing ratio); a large input confirms
622+
// it counts correctly and finishes quickly on the linear path.
623+
try std.testing.expect(re.engine_type == .thompson_nfa);
679624

680-
try std.testing.expectEqual(@as(usize, 20_000), try re.count(b1));
681-
try std.testing.expectEqual(@as(usize, 40_000), try re.count(b2));
682-
683-
const t1 = try time.run(&re, b1);
684-
const t2 = try time.run(&re, b2);
685-
try std.testing.expect(t1 > 0);
686-
try std.testing.expect(t2 * 10 < t1 * 30); // < 3.0x — linear, not quadratic
625+
var list = try std.ArrayList(u8).initCapacity(allocator, 800_000);
626+
defer list.deinit(allocator);
627+
var i: usize = 0;
628+
while (i < 40_000) : (i += 1) try list.appendSlice(allocator, "x = 1; // note here\n");
629+
try std.testing.expectEqual(@as(usize, 40_000), try re.count(list.items));
687630
}
688631

689632
test "regression: literal alternation preserves source order" {

0 commit comments

Comments
 (0)