@@ -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
586557test "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\n z" , 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
689632test "regression: literal alternation preserves source order" {
0 commit comments