Source
tests/purrc/regex_e2e_test.cpp
1
// Copyright (c) 2026 BigBrain LLC. MIT-licensed (see LICENSE).2
// Original work; see ACKNOWLEDGMENTS.md for the open-source ideas we build upon.3
// regex end-to-end adversarial suite (suite RegexE2E).4
//5
// Each test compiles a real .purr program with purrc and runs it under the cheatah runtime, checking6
// exact stdout — so this exercises the WHOLE pipeline (compiler + runtime + libcheatah_regex.a) the7
// way a user hits it. The emphasis is adversarial: catastrophic-backtracking (ReDoS) inputs that must8
// stay fast and correct, anchoring/quantifier/class edge cases, and the leftmost-longest contract.9
//10
// Every expected string here was captured from the built engine (build/debug) — not guessed.11
// `regex.find` returns `.text` as an OWNED `str` (the library copies the matched bytes), so it is12
// always safe — no borrow, no dangling, even off a temporary input.13
//14
// These are NOT named *CompileRun*, so they run by default in the gate (ctest stage). They are fast15
// (small inputs; the DFA is linear, so even the ReDoS rows return in microseconds).17
#include "e2e_harness.hpp"19
using e2e::expect_e2e;21
// ── yes/no answers: search (unanchored) and full_match (anchored both ends) ───────────────23
TEST(RegexE2E, SearchPresentAbsent) {24
expect_e2e("regex_search", R"PURR(import io25
import regex26
let d = regex.compile("[0-9]+")27
io.print(regex.search(d, "order 4567 shipped"))28
io.print(regex.search(d, "no digits here"))29
)PURR",30
"True\nFalse\n");31
}33
TEST(RegexE2E, FullMatchIsAnchoredBothEnds) {34
expect_e2e("regex_fullmatch", R"PURR(import io35
import regex36
let d = regex.compile("[0-9]+")37
io.print(regex.full_match(d, "4567"))38
io.print(regex.full_match(d, "x4567"))39
io.print(regex.full_match(d, "4567x"))40
)PURR",41
"True\nFalse\nFalse\n");42
}44
// ── find: leftmost-longest match, offsets, and the OWNED matched bytes ─────────────────────46
TEST(RegexE2E, FindOffsetsAndOwnedText) {47
expect_e2e("regex_find", R"PURR(import io48
import regex49
let email = regex.compile("[a-z]+@[a-z.]+")50
let text = "contact bob@example.com now"51
let m = regex.find(email, text)52
io.print(m.matched)53
io.print(m.begin, m.end)54
io.print(m.text)55
io.print(m.text)56
)PURR",57
"True\n8 23\nbob@example.com\nbob@example.com\n");58
}60
TEST(RegexE2E, FindIsLeftmostLongest) {61
expect_e2e("regex_greedy", R"PURR(import io62
import regex63
let g = regex.compile("a+")64
let s = "baaab"65
let m = regex.find(g, s)66
io.print(m.text, m.begin, m.end)67
)PURR",68
"aaa 1 4\n");69
}71
TEST(RegexE2E, FindAlternationLeftmost) {72
expect_e2e("regex_alt", R"PURR(import io73
import regex74
let alt = regex.compile("cat|dog|bird")75
let s = "I have a dog and a cat"76
let m = regex.find(alt, s)77
io.print(m.text, m.begin, m.end)78
)PURR",79
"dog 9 12\n");80
}82
// ── the whole point: adversarial ReDoS inputs stay fast AND correct (a backtracker hangs) ──84
TEST(RegexE2E, ReDoSNestedQuantifierReturnsFalseFast) {85
// (a+)+$ over a long run of 'a' ending in '!' — a classic catastrophic-backtracking pattern.86
// A backtracking engine explores ~2^n paths; the lazy DFA answers in linear time.87
expect_e2e("regex_redos1", R"PURR(import io88
import regex89
let evil = regex.compile("(a+)+$")90
let s = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa!"91
io.print(regex.search(evil, s))92
)PURR",93
"False\n");94
}96
TEST(RegexE2E, ReDoSAlternationStarReturnsFalseFast) {97
expect_e2e("regex_redos2", R"PURR(import io98
import regex99
let evil = regex.compile("(a|a)*c")100
let s = "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"101
io.print(regex.search(evil, s))102
)PURR",103
"False\n");104
}106
// ── anchors ───────────────────────────────────────────────────────────────────────────────108
TEST(RegexE2E, Anchors) {109
expect_e2e("regex_anchors", R"PURR(import io110
import regex111
io.print(regex.search(regex.compile("^foo"), "foo bar"))112
io.print(regex.search(regex.compile("^foo"), "bar foo"))113
io.print(regex.search(regex.compile("bar$"), "foo bar"))114
io.print(regex.search(regex.compile("bar$"), "bar foo"))115
)PURR",116
"True\nFalse\nTrue\nFalse\n");117
}119
// ── empty matches: a pattern that can match "" matches EVERYWHERE the anchors allow ───────120
// (Pins the a*$ hole: the first-byte candidate scan must not skip the empty match at121
// end-of-input — see PreviouslyBroken.RegexEmptyMatchAtEndOfInput for the full story.)123
TEST(RegexE2E, EmptyMatchAtEveryAnchoring) {124
expect_e2e("regex_empty", R"PURR(import io125
import regex126
io.print(regex.search(regex.compile("a*$"), "bbb"))127
io.print(regex.search(regex.compile("x*"), "yyy"))128
io.print(regex.search(regex.compile("^$"), ""))129
io.print(regex.search(regex.compile("^$"), "a"))130
io.print(regex.search(regex.compile("$"), "abc"))131
let m = regex.find(regex.compile("x*"), "yyy")132
io.print(m.matched, m.begin, m.end)133
)PURR",134
"True\n" // a*$ — empty match at end-of-input135
"True\n" // x* unanchored — empty match at position 0136
"True\n" // ^$ on "" — the empty input matches137
"False\n" // ^$ on "a" — both anchors can NOT hold around a byte138
"True\n" // bare $ — empty match at end of any input139
"True 0 0\n"); // find pins the leftmost empty match at [0,0)140
}142
// ── quantifiers: * + ? ─────────────────────────────────────────────────────────────────────144
TEST(RegexE2E, Quantifiers) {145
expect_e2e("regex_quant", R"PURR(import io146
import regex147
let plus = regex.compile("ab+c")148
io.print(regex.search(plus, "ac"))149
io.print(regex.search(plus, "abc"))150
io.print(regex.search(plus, "abbbbc"))151
io.print(regex.full_match(regex.compile("ab*c"), "ac"))152
io.print(regex.full_match(regex.compile("colou?r"), "color"))153
io.print(regex.full_match(regex.compile("colou?r"), "colour"))154
)PURR",155
"False\nTrue\nTrue\nTrue\nTrue\nTrue\n");156
}158
// ── character classes, negation, and the \d \w \s escapes ─────────────────────────────────160
TEST(RegexE2E, CharacterClasses) {161
expect_e2e("regex_classes", R"PURR(import io162
import regex163
io.print(regex.full_match(regex.compile("[^0-9]+"), "abcDEF"))164
io.print(regex.full_match(regex.compile("\\d+"), "12345"))165
io.print(regex.full_match(regex.compile("\\w+"), "abc_123"))166
io.print(regex.search(regex.compile("\\s"), "a b"))167
io.print(regex.full_match(regex.compile("[A-Z]+"), "abc"))168
io.print(regex.full_match(regex.compile("[A-Z]+"), "ABC"))169
)PURR",170
"True\nTrue\nTrue\nTrue\nFalse\nTrue\n");171
}173
// ── `.` matches any byte EXCEPT newline ────────────────────────────────────────────────────175
TEST(RegexE2E, DotExcludesNewline) {176
expect_e2e("regex_dot", R"PURR(import io177
import regex178
let dot = regex.compile("a.b")179
io.print(regex.search(dot, "axb"))180
io.print(regex.search(dot, "a\nb"))181
)PURR",182
"True\nFalse\n");183
}185
// ── escaped metacharacters are literals ────────────────────────────────────────────────────187
TEST(RegexE2E, EscapedDotIsLiteral) {188
expect_e2e("regex_escape", R"PURR(import io189
import regex190
let re = regex.compile("a\\.b")191
io.print(regex.full_match(re, "a.b"))192
io.print(regex.full_match(re, "axb"))193
)PURR",194
"True\nFalse\n");195
}197
// ── a malformed pattern is rejected, not thrown: Pattern.ok == false ───────────────────────199
TEST(RegexE2E, MalformedPatternReportsNotOk) {200
expect_e2e("regex_bad", R"PURR(import io201
import regex202
io.print(regex.compile("[0-9]+").ok)203
io.print(regex.compile("[").ok)204
)PURR",205
"True\nFalse\n");206
}208
// ── a worked loop: pull every number out of a line by advancing past each match ────────────210
TEST(RegexE2E, FindAllNumbersByAdvancing) {211
expect_e2e("regex_findall", R"PURR(import io212
import regex213
let re = regex.compile("[0-9]+")214
let line = "id=48213 status=200 bytes=1274"215
let rest = line216
for _ in range(0, 10) {217
let f = regex.find(re, rest)218
if not f.matched { break }219
io.print(f.text)220
rest = rest[f.end:]221
}222
)PURR",223
"48213\n200\n1274\n");224
}