From 05a42426d933fe689cdf36ee87984ff341474177 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 10 Dec 2025 20:52:59 +0000 Subject: [PATCH] Add regex_match benchmark for pattern matching on large text Implements regex matching benchmark across all supported languages: - C: Uses POSIX regex library - Python: Uses built-in re module - JavaScript: Uses native RegExp - Ruby: Uses built-in Regexp - Hemlock: Manual pattern matching implementation (no built-in regex) Tests multiple patterns: email, phone, date, price, URL, IP, code, and words. --- CLAUDE.md | 1 + benchmarks.json | 5 + benchmarks/regex_match/regex_match.c | 81 ++++++ benchmarks/regex_match/regex_match.hml | 340 +++++++++++++++++++++++++ benchmarks/regex_match/regex_match.js | 33 +++ benchmarks/regex_match/regex_match.py | 32 +++ benchmarks/regex_match/regex_match.rb | 29 +++ run.sh | 7 +- 8 files changed, 526 insertions(+), 2 deletions(-) create mode 100644 benchmarks/regex_match/regex_match.c create mode 100644 benchmarks/regex_match/regex_match.hml create mode 100644 benchmarks/regex_match/regex_match.js create mode 100644 benchmarks/regex_match/regex_match.py create mode 100644 benchmarks/regex_match/regex_match.rb diff --git a/CLAUDE.md b/CLAUDE.md index 515936a..887399b 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -30,6 +30,7 @@ HEMLOCKC_BIN="../hemlock/hemlockc" ./run.sh - **primes_sieve** - Sieve of Eratosthenes (array access, loops) - **json_serialize** - JSON serialization (object creation, string building) - **json_deserialize** - JSON parsing (string parsing, object creation) +- **regex_match** - Regular expression matching on large text (pattern matching, string scanning) ## Notes diff --git a/benchmarks.json b/benchmarks.json index 288ad83..1ae703b 100644 --- a/benchmarks.json +++ b/benchmarks.json @@ -19,6 +19,11 @@ "description": "Sieve of Eratosthenes", "default_input": 1000000, "quick_input": 100000 + }, + "regex_match": { + "description": "Regular expression matching", + "default_input": 10000, + "quick_input": 1000 } }, "languages": { diff --git a/benchmarks/regex_match/regex_match.c b/benchmarks/regex_match/regex_match.c new file mode 100644 index 0000000..485854c --- /dev/null +++ b/benchmarks/regex_match/regex_match.c @@ -0,0 +1,81 @@ +#include +#include +#include +#include + +// Generate test text with patterns to match +char* generate_text(int iterations) { + const char* base = "The quick brown fox jumps over the lazy dog. " + "Email: user123@example.com Phone: 555-1234 " + "Date: 2024-01-15 Price: $99.99 " + "URL: https://www.example.com/path?query=value " + "IP: 192.168.1.1 Code: ABC-123-XYZ\n"; + size_t base_len = strlen(base); + size_t total_len = base_len * iterations; + + char* text = malloc(total_len + 1); + if (!text) return NULL; + + char* ptr = text; + for (int i = 0; i < iterations; i++) { + memcpy(ptr, base, base_len); + ptr += base_len; + } + *ptr = '\0'; + + return text; +} + +// Count all matches of a pattern in text +int count_matches(const char* text, const char* pattern) { + regex_t regex; + regmatch_t match; + int count = 0; + + if (regcomp(®ex, pattern, REG_EXTENDED) != 0) { + return 0; + } + + const char* ptr = text; + while (regexec(®ex, ptr, 1, &match, 0) == 0) { + count++; + ptr += match.rm_eo; + if (*ptr == '\0') break; + } + + regfree(®ex); + return count; +} + +int main(int argc, char* argv[]) { + int iterations = argc > 1 ? atoi(argv[1]) : 10000; + + char* text = generate_text(iterations); + if (!text) { + fprintf(stderr, "Failed to allocate memory\n"); + return 1; + } + + // Various regex patterns to test different matching scenarios + const char* patterns[] = { + "[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}", // Email + "[0-9]{3}-[0-9]{4}", // Phone + "[0-9]{4}-[0-9]{2}-[0-9]{2}", // Date + "\\$[0-9]+\\.[0-9]{2}", // Price + "https?://[a-zA-Z0-9.-]+/[a-zA-Z0-9/?=&._-]*", // URL + "[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}\\.[0-9]{1,3}", // IP + "[A-Z]{3}-[0-9]{3}-[A-Z]{3}", // Code + "\\b[a-z]{4,}\\b" // 4+ letter words + }; + int num_patterns = sizeof(patterns) / sizeof(patterns[0]); + + int total_matches = 0; + for (int i = 0; i < num_patterns; i++) { + total_matches += count_matches(text, patterns[i]); + } + + printf("%d\n", total_matches); + + free(text); + return 0; +} diff --git a/benchmarks/regex_match/regex_match.hml b/benchmarks/regex_match/regex_match.hml new file mode 100644 index 0000000..5edc8b4 --- /dev/null +++ b/benchmarks/regex_match/regex_match.hml @@ -0,0 +1,340 @@ +// Regex matching benchmark - Hemlock +// Since Hemlock doesn't have built-in regex, we implement pattern matchers manually + +fn parse_int(s) { + let val = 0; + let idx = 0; + while (idx < s.length) { + let ch = s[idx]; + let code: i32 = ch; + let zero: i32 = '0'; + val = val * 10 + (code - zero); + idx = idx + 1; + } + return val; +} + +fn is_digit(ch) { + let code: i32 = ch; + let zero: i32 = '0'; + let nine: i32 = '9'; + return code >= zero && code <= nine; +} + +fn is_lower(ch) { + let code: i32 = ch; + let a: i32 = 'a'; + let z: i32 = 'z'; + return code >= a && code <= z; +} + +fn is_upper(ch) { + let code: i32 = ch; + let a: i32 = 'A'; + let z: i32 = 'Z'; + return code >= a && code <= z; +} + +fn is_alpha(ch) { + return is_lower(ch) || is_upper(ch); +} + +fn is_alnum(ch) { + return is_alpha(ch) || is_digit(ch); +} + +fn is_email_char(ch) { + if (is_alnum(ch)) { return true; } + if (ch == '.') { return true; } + if (ch == '_') { return true; } + if (ch == '%') { return true; } + if (ch == '+') { return true; } + if (ch == '-') { return true; } + return false; +} + +fn is_domain_char(ch) { + if (is_alnum(ch)) { return true; } + if (ch == '.') { return true; } + if (ch == '-') { return true; } + return false; +} + +fn is_url_char(ch) { + if (is_alnum(ch)) { return true; } + if (ch == '/') { return true; } + if (ch == '?') { return true; } + if (ch == '=') { return true; } + if (ch == '&') { return true; } + if (ch == '.') { return true; } + if (ch == '_') { return true; } + if (ch == '-') { return true; } + return false; +} + +// Match email pattern: [a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,} +fn match_email(text, start) { + let pos = start; + let local_len = 0; + + // Match local part + while (pos < text.length && is_email_char(text[pos])) { + local_len = local_len + 1; + pos = pos + 1; + } + if (local_len == 0) { return -1; } + + // Match @ + if (pos >= text.length || text[pos] != '@') { return -1; } + pos = pos + 1; + + // Match domain + let domain_len = 0; + let last_dot = -1; + let domain_start = pos; + while (pos < text.length && is_domain_char(text[pos])) { + if (text[pos] == '.') { + last_dot = pos - domain_start; + } + domain_len = domain_len + 1; + pos = pos + 1; + } + if (domain_len < 3 || last_dot < 1) { return -1; } + + return pos; +} + +// Match phone pattern: [0-9]{3}-[0-9]{4} +fn match_phone(text, start) { + let pos = start; + + // Match 3 digits + let i = 0; + while (i < 3) { + if (pos >= text.length || !is_digit(text[pos])) { return -1; } + pos = pos + 1; + i = i + 1; + } + + // Match dash + if (pos >= text.length || text[pos] != '-') { return -1; } + pos = pos + 1; + + // Match 4 digits + i = 0; + while (i < 4) { + if (pos >= text.length || !is_digit(text[pos])) { return -1; } + pos = pos + 1; + i = i + 1; + } + + return pos; +} + +// Match date pattern: [0-9]{4}-[0-9]{2}-[0-9]{2} +fn match_date(text, start) { + let pos = start; + + // Match 4 digits (year) + let i = 0; + while (i < 4) { + if (pos >= text.length || !is_digit(text[pos])) { return -1; } + pos = pos + 1; + i = i + 1; + } + + // Match dash + if (pos >= text.length || text[pos] != '-') { return -1; } + pos = pos + 1; + + // Match 2 digits (month) + i = 0; + while (i < 2) { + if (pos >= text.length || !is_digit(text[pos])) { return -1; } + pos = pos + 1; + i = i + 1; + } + + // Match dash + if (pos >= text.length || text[pos] != '-') { return -1; } + pos = pos + 1; + + // Match 2 digits (day) + i = 0; + while (i < 2) { + if (pos >= text.length || !is_digit(text[pos])) { return -1; } + pos = pos + 1; + i = i + 1; + } + + return pos; +} + +// Match price pattern: \$[0-9]+\.[0-9]{2} +fn match_price(text, start) { + let pos = start; + + // Match $ + if (pos >= text.length || text[pos] != '$') { return -1; } + pos = pos + 1; + + // Match digits + let digit_count = 0; + while (pos < text.length && is_digit(text[pos])) { + digit_count = digit_count + 1; + pos = pos + 1; + } + if (digit_count == 0) { return -1; } + + // Match dot + if (pos >= text.length || text[pos] != '.') { return -1; } + pos = pos + 1; + + // Match 2 digits + let i = 0; + while (i < 2) { + if (pos >= text.length || !is_digit(text[pos])) { return -1; } + pos = pos + 1; + i = i + 1; + } + + return pos; +} + +// Match IP pattern: [0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3} +fn match_ip(text, start) { + let pos = start; + + let octet = 0; + while (octet < 4) { + // Match 1-3 digits + let digit_count = 0; + while (digit_count < 3 && pos < text.length && is_digit(text[pos])) { + digit_count = digit_count + 1; + pos = pos + 1; + } + if (digit_count == 0) { return -1; } + + // Match dot (except after last octet) + if (octet < 3) { + if (pos >= text.length || text[pos] != '.') { return -1; } + pos = pos + 1; + } + + octet = octet + 1; + } + + return pos; +} + +// Match code pattern: [A-Z]{3}-[0-9]{3}-[A-Z]{3} +fn match_code(text, start) { + let pos = start; + + // Match 3 uppercase letters + let i = 0; + while (i < 3) { + if (pos >= text.length || !is_upper(text[pos])) { return -1; } + pos = pos + 1; + i = i + 1; + } + + // Match dash + if (pos >= text.length || text[pos] != '-') { return -1; } + pos = pos + 1; + + // Match 3 digits + i = 0; + while (i < 3) { + if (pos >= text.length || !is_digit(text[pos])) { return -1; } + pos = pos + 1; + i = i + 1; + } + + // Match dash + if (pos >= text.length || text[pos] != '-') { return -1; } + pos = pos + 1; + + // Match 3 uppercase letters + i = 0; + while (i < 3) { + if (pos >= text.length || !is_upper(text[pos])) { return -1; } + pos = pos + 1; + i = i + 1; + } + + return pos; +} + +// Match 4+ letter word at word boundary +fn match_word4plus(text, start) { + // Check word boundary (start of text or prev char is not alpha) + if (start > 0 && is_alpha(text[start - 1])) { + return -1; + } + + let pos = start; + let len = 0; + + // Match lowercase letters + while (pos < text.length && is_lower(text[pos])) { + len = len + 1; + pos = pos + 1; + } + + // Check we have at least 4 letters + if (len < 4) { return -1; } + + // Check word boundary (end of text or next char is not alpha) + if (pos < text.length && is_alpha(text[pos])) { + return -1; + } + + return pos; +} + +// Count matches of a pattern in text +fn count_matches(text, matcher) { + let count = 0; + let pos = 0; + + while (pos < text.length) { + let end = matcher(text, pos); + if (end > pos) { + count = count + 1; + pos = end; + } else { + pos = pos + 1; + } + } + + return count; +} + +// Main +let iterations = 10000; +if (args.length > 1) { + iterations = parse_int(args[1]); +} + +// Generate test text +let base = "The quick brown fox jumps over the lazy dog. Email: user123@example.com Phone: 555-1234 Date: 2024-01-15 Price: $99.99 URL: https://www.example.com/path?query=value IP: 192.168.1.1 Code: ABC-123-XYZ\n"; + +let text = ""; +let i = 0; +while (i < iterations) { + text = text + base; + i = i + 1; +} + +// Count all pattern matches +let total = 0; +total = total + count_matches(text, match_email); +total = total + count_matches(text, match_phone); +total = total + count_matches(text, match_date); +total = total + count_matches(text, match_price); +total = total + count_matches(text, match_ip); +total = total + count_matches(text, match_code); +total = total + count_matches(text, match_word4plus); + +print(total); diff --git a/benchmarks/regex_match/regex_match.js b/benchmarks/regex_match/regex_match.js new file mode 100644 index 0000000..ee4ebb8 --- /dev/null +++ b/benchmarks/regex_match/regex_match.js @@ -0,0 +1,33 @@ +const iterations = parseInt(process.argv[2]) || 10000; + +// Generate test text with patterns to match +const base = `The quick brown fox jumps over the lazy dog. \ +Email: user123@example.com Phone: 555-1234 \ +Date: 2024-01-15 Price: $99.99 \ +URL: https://www.example.com/path?query=value \ +IP: 192.168.1.1 Code: ABC-123-XYZ +`; + +const text = base.repeat(iterations); + +// Various regex patterns to test different matching scenarios +const patterns = [ + /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/g, // Email + /[0-9]{3}-[0-9]{4}/g, // Phone + /[0-9]{4}-[0-9]{2}-[0-9]{2}/g, // Date + /\$[0-9]+\.[0-9]{2}/g, // Price + /https?:\/\/[a-zA-Z0-9.-]+\/[a-zA-Z0-9/?=&._-]*/g, // URL + /[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/g, // IP + /[A-Z]{3}-[0-9]{3}-[A-Z]{3}/g, // Code + /\b[a-z]{4,}\b/g // 4+ letter words +]; + +let totalMatches = 0; +for (const pattern of patterns) { + const matches = text.match(pattern); + if (matches) { + totalMatches += matches.length; + } +} + +console.log(totalMatches); diff --git a/benchmarks/regex_match/regex_match.py b/benchmarks/regex_match/regex_match.py new file mode 100644 index 0000000..c3dd6d9 --- /dev/null +++ b/benchmarks/regex_match/regex_match.py @@ -0,0 +1,32 @@ +import sys +import re + +iterations = int(sys.argv[1]) if len(sys.argv) > 1 else 10000 + +# Generate test text with patterns to match +base = """The quick brown fox jumps over the lazy dog. \ +Email: user123@example.com Phone: 555-1234 \ +Date: 2024-01-15 Price: $99.99 \ +URL: https://www.example.com/path?query=value \ +IP: 192.168.1.1 Code: ABC-123-XYZ +""" + +text = base * iterations + +# Various regex patterns to test different matching scenarios +patterns = [ + r'[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}', # Email + r'[0-9]{3}-[0-9]{4}', # Phone + r'[0-9]{4}-[0-9]{2}-[0-9]{2}', # Date + r'\$[0-9]+\.[0-9]{2}', # Price + r'https?://[a-zA-Z0-9.-]+/[a-zA-Z0-9/?=&._-]*', # URL + r'[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}', # IP + r'[A-Z]{3}-[0-9]{3}-[A-Z]{3}', # Code + r'\b[a-z]{4,}\b' # 4+ letter words +] + +total_matches = 0 +for pattern in patterns: + total_matches += len(re.findall(pattern, text)) + +print(total_matches) diff --git a/benchmarks/regex_match/regex_match.rb b/benchmarks/regex_match/regex_match.rb new file mode 100644 index 0000000..2c3c73b --- /dev/null +++ b/benchmarks/regex_match/regex_match.rb @@ -0,0 +1,29 @@ +iterations = ARGV[0] ? ARGV[0].to_i : 10000 + +# Generate test text with patterns to match +base = "The quick brown fox jumps over the lazy dog. " \ + "Email: user123@example.com Phone: 555-1234 " \ + "Date: 2024-01-15 Price: $99.99 " \ + "URL: https://www.example.com/path?query=value " \ + "IP: 192.168.1.1 Code: ABC-123-XYZ\n" + +text = base * iterations + +# Various regex patterns to test different matching scenarios +patterns = [ + /[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}/, # Email + /[0-9]{3}-[0-9]{4}/, # Phone + /[0-9]{4}-[0-9]{2}-[0-9]{2}/, # Date + /\$[0-9]+\.[0-9]{2}/, # Price + /https?:\/\/[a-zA-Z0-9.-]+\/[a-zA-Z0-9\/?=&._-]*/, # URL + /[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}/, # IP + /[A-Z]{3}-[0-9]{3}-[A-Z]{3}/, # Code + /\b[a-z]{4,}\b/ # 4+ letter words +] + +total_matches = 0 +patterns.each do |pattern| + total_matches += text.scan(pattern).length +end + +puts total_matches diff --git a/run.sh b/run.sh index 06bcb6f..84d7e3a 100755 --- a/run.sh +++ b/run.sh @@ -28,7 +28,7 @@ usage() { echo " --iter N Number of iterations (default: 3)" echo " --help, -h Show this help" echo "" - echo "Benchmarks: fib, array_sum, string_concat, primes_sieve, json_serialize, json_deserialize" + echo "Benchmarks: fib, array_sum, string_concat, primes_sieve, json_serialize, json_deserialize, regex_match" echo " (leave empty to run all)" } @@ -78,6 +78,9 @@ get_input() { json_deserialize) [[ $QUICK_MODE -eq 1 ]] && echo 10000 || echo 100000 ;; + regex_match) + [[ $QUICK_MODE -eq 1 ]] && echo 1000 || echo 10000 + ;; esac } @@ -201,7 +204,7 @@ run_all() { if [[ -n "$BENCHMARK" ]]; then benchmarks="$BENCHMARK" else - benchmarks="fib array_sum string_concat primes_sieve json_serialize json_deserialize" + benchmarks="fib array_sum string_concat primes_sieve json_serialize json_deserialize regex_match" fi local languages="c hemlockc hemlock python javascript ruby"