zig-regex

Advanced Features

On this page 27

This document describes the advanced features available in the zig-regex library.

Table of Contents

Regex Macros & Composition

The macro system allows you to define reusable pattern components and compose them into larger patterns.

Basic Usage

const std = @import("std");
const macros = @import("regex").macros;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Create a macro registry
    var registry = macros.MacroRegistry.init(allocator);
    defer registry.deinit();

    // Define macros
    try registry.define("digit", "[0-9]");
    try registry.define("word", "[a-zA-Z]+");
    try registry.define("email_local", "[a-zA-Z0-9._%+-]+");
    try registry.define("email_domain", "[a-zA-Z0-9.-]+\\.[a-zA-Z]{2,}");

    // Expand macros in patterns
    const phone_pattern = try registry.expand("\\(?${digit}{3}\\)?[-. ]?${digit}{3}[-. ]?${digit}{4}");
    defer allocator.free(phone_pattern);

    const email_pattern = try registry.expand("${email_local}@${email_domain}");
    defer allocator.free(email_pattern);

    std.debug.print("Phone pattern: {s}\n", .{phone_pattern});
    std.debug.print("Email pattern: {s}\n", .{email_pattern});
}

Common Predefined Macros

const macros = @import("regex").macros;

// Load all common macros
var registry = macros.MacroRegistry.init(allocator);
try macros.CommonMacros.loadInto(&registry);

// Available macros:
// - digit: [0-9]
// - word: [a-zA-Z]
// - word_char: [a-zA-Z0-9_]
// - whitespace: [ \t\r\n]
// - alpha: [a-zA-Z]
// - alnum: [a-zA-Z0-9]
// - hex: [0-9a-fA-F]
// - lower: [a-z]
// - upper: [A-Z]
// - email_local: [a-zA-Z0-9._%+-]+
// - email_domain: [a-zA-Z0-9.-]+\.[a-zA-Z]{2,}
// - ipv4_octet: (?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)
// - url_scheme: https?

Type-Safe Builder API

The builder API provides a fluent interface for constructing regex patterns programmatically, reducing errors and improving readability.

Basic Example

const std = @import("std");
const Builder = @import("regex").Builder;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    // Create a builder
    var builder = Builder.init(allocator);
    defer builder.deinit();

    // Build a pattern: ^\d{3}-\d{3}-\d{4}$
    _ = try builder
        .startOfLine()
        .digit().repeatExact(3)
        .literal("-")
        .digit().repeatExact(3)
        .literal("-")
        .digit().repeatExact(4)
        .endOfLine();

    // Compile the pattern
    var regex = try builder.compile();
    defer regex.deinit();

    // Use it
    if (try regex.isMatch("555-123-4567")) {
        std.debug.print("Valid phone number!\n", .{});
    }
}

Builder Methods

// Literals and character classes
.literal("text")        // Escaped literal text
.any()                  // Match any character (.)
.digit()                // Match digit (\d)
.word()                 // Match word character (\w)
.whitespace()           // Match whitespace (\s)
.charClass("abc")       // Custom character class [abc]
.notCharClass("abc")    // Negated class [^abc]
.charRange('a', 'z')    // Character range [a-z]

// Quantifiers
.oneOrMore()            // + (greedy)
.zeroOrMore()           // _ (greedy)
.optional()             // ? (greedy)
.repeatExact(n)         // {n}
.repeatAtLeast(n)       // {n,}
.repeatRange(min, max)  // {min,max}

// Anchors and boundaries
.startOfLine()          // ^
.endOfLine()            // $
.wordBoundary()         // \b

// Groups and alternation
.startGroup()           // (
.endGroup()             // )
.startNonCapturingGroup() // (?:
.or_()                  // |

Predefined Patterns

const Patterns = @import("regex").Patterns;

// Email validation
const email_pattern = try Patterns.email(allocator);
defer allocator.free(email_pattern);

// Available patterns:
// - email(allocator)
// - url(allocator)
// - ipv4(allocator)
// - phoneUS(allocator)
// - dateISO(allocator)  // YYYY-MM-DD
// - time24(allocator)   // HH:MM or HH:MM:SS
// - hexColor(allocator) // #RGB or #RRGGBB
// - creditCard(allocator)
// - uuid(allocator)
// - integer(allocator)
// - decimal(allocator)
// - identifier(allocator) // Variable names

Pattern Composition

const Composer = @import("regex").Composer;

var composer = Composer.init(allocator);
defer composer.deinit();

// Add patterns
_ = try composer.add("foo");
_ = try composer.add("bar");
_ = try composer.add("baz");

// Compose with alternation: foo|bar|baz
const alt_pattern = try composer.alternatives();
defer allocator.free(alt_pattern);

// Compose with concatenation: foobarbaz
const seq_pattern = try composer.sequence();
defer allocator.free(seq_pattern);

// Wrap in a group: (foo|bar|baz)
const grouped = try composer.group();
defer allocator.free(grouped);

C FFI

The C API provides a stable ABI for using zig-regex from other languages.

C Header (c_api.h)

# include <stddef.h>
# include <stdbool.h>

typedef struct ZigRegex ZigRegex;
typedef struct ZigMatch ZigMatch;
typedef struct ZigMatchArray ZigMatchArray;

typedef struct {
    bool case_insensitive;
    bool multiline;
    bool dot_all;
    bool unicode;
} ZigRegexFlags;

typedef struct {
    size_t start;
    size_t end;
    size_t capture_count;
} ZigMatchInfo;

typedef enum {
    ZIG_REGEX_SUCCESS = 0,
    ZIG_REGEX_OUT_OF_MEMORY = 1,
    ZIG_REGEX_INVALID_PATTERN = 2,
    ZIG_REGEX_COMPILATION_FAILED = 3,
    ZIG_REGEX_MATCH_FAILED = 4,
    ZIG_REGEX_INVALID_HANDLE = 5,
    ZIG_REGEX_BUFFER_TOO_SMALL = 6,
} ZigRegexError;

// Compile a regex
ZigRegex_ zig_regex_compile(const char_ pattern, ZigRegexFlags flags, ZigRegexError_ error_code);

// Free a regex
void zig_regex_free(ZigRegex_ regex);

// Test if pattern matches
bool zig_regex_is_match(ZigRegex_ regex, const char_ input, ZigRegexError_ error_code);

// Find first match
ZigMatch_ zig_regex_find(ZigRegex_ regex, const char_ input, ZigRegexError_ error_code);

// Get match info
ZigRegexError zig_match_get_info(ZigMatch_ match, ZigMatchInfo_ info);

// Get matched text
size_t zig_match_get_text(ZigMatch_ match, char_ buffer, size_t buffer_len);

// Get capture group
size_t zig_match_get_capture(ZigMatch_ match, size_t capture_index, char_ buffer, size_t buffer_len);

// Free match
void zig_match_free(ZigMatch_ match);

// Find all matches
ZigMatchArray_ zig_regex_find_all(ZigRegex_ regex, const char_ input, ZigRegexError_ error_code);

// Get array length
size_t zig_match_array_len(ZigMatchArray_ matches);

// Get match from array
ZigMatch_ zig_match_array_get(ZigMatchArray_ matches, size_t index);

// Free match array
void zig_match_array_free(ZigMatchArray_ matches);

// Replace first match
size_t zig_regex_replace(ZigRegex_ regex, const char_ input, const char_ replacement,
                         char_ buffer, size_t buffer_len, ZigRegexError_ error_code);

// Get library version
const char_ zig_regex_version();

C Usage Example

# include "c_api.h"
# include <stdio.h>
# include <stdlib.h>
# include <string.h>

int main() {
    ZigRegexFlags flags = {
        .case_insensitive = false,
        .multiline = false,
        .dot_all = false,
        .unicode = false,
    };

    ZigRegexError error;

    // Compile regex
    ZigRegex_ regex = zig_regex_compile("\\d{3}-\\d{3}-\\d{4}", flags, &error);
    if (!regex) {
        fprintf(stderr, "Failed to compile regex: %d\n", error);
        return 1;
    }

    // Test match
    const char_ input = "Call me at 555-123-4567";
    ZigMatch_ match = zig_regex_find(regex, input, &error);

    if (match) {
        // Get match info
        ZigMatchInfo info;
        zig_match_get_info(match, &info);

        // Get matched text
        char buffer[256];
        size_t len = zig_match_get_text(match, buffer, sizeof(buffer));

        printf("Found match: %s (at position %zu-%zu)\n", buffer, info.start, info.end);

        zig_match_free(match);
    } else {
        printf("No match found\n");
    }

    zig_regex_free(regex);
    return 0;
}

Python FFI Example

import ctypes
import os

# Load the shared library
lib = ctypes.CDLL("./zig-out/lib/libzig-regex.so")

# Define structures
class ZigRegexFlags(ctypes.Structure):
    _fields_ = [
        ("case_insensitive", ctypes.c_bool),
        ("multiline", ctypes.c_bool),
        ("dot_all", ctypes.c_bool),
        ("unicode", ctypes.c_bool),
    ]

# Define function signatures
lib.zig_regex_compile.argtypes = [ctypes.c_char_p, ZigRegexFlags, ctypes.POINTER(ctypes.c_int)]
lib.zig_regex_compile.restype = ctypes.c_void_p

lib.zig_regex_is_match.argtypes = [ctypes.c_void_p, ctypes.c_char_p, ctypes.POINTER(ctypes.c_int)]
lib.zig_regex_is_match.restype = ctypes.c_bool

lib.zig_regex_free.argtypes = [ctypes.c_void_p]
lib.zig_regex_free.restype = None

# Use it
flags = ZigRegexFlags(case_insensitive=False, multiline=False, dot_all=False, unicode=False)
error = ctypes.c_int()

regex = lib.zig_regex_compile(b"\\d+", flags, ctypes.byref(error))

if regex:
    is_match = lib.zig_regex_is_match(regex, b"hello123", ctypes.byref(error))
    print(f"Match: {is_match}")
    lib.zig_regex_free(regex)

WASM Support

The library can be compiled to WebAssembly for use in browsers and Node.js.

Building for WASM

# Build for WASM
zig build-lib src/c_api.zig -target wasm32-freestanding -dynamic -rdynamic

# Or use the provided build script
zig build wasm

JavaScript Usage

// Load WASM module
const wasmModule = await WebAssembly.instantiateStreaming(
    fetch('zig-regex.wasm')
);

const {
    zig_regex_compile,
    zig_regex_is_match,
    zig_regex_free,
    memory
} = wasmModule.instance.exports;

// Helper to convert JS string to WASM memory
function stringToWasm(str) {
    const bytes = new TextEncoder().encode(str + '\0');
    const ptr = allocate(bytes.length);
    new Uint8Array(memory.buffer, ptr, bytes.length).set(bytes);
    return ptr;
}

// Compile regex
const pattern = stringToWasm('\\d+');
const flags = 0; // No flags
const errorPtr = allocate(4);
const regex = zig_regex_compile(pattern, flags, errorPtr);

// Test match
const input = stringToWasm('hello123');
const isMatch = zig_regex_is_match(regex, input, errorPtr);

console.log('Match:', isMatch);

// Clean up
zig_regex_free(regex);

Regex Lint & Analysis

The linter analyzes regex patterns and provides warnings about potential issues.

Basic Usage

const std = @import("std");
const Linter = @import("regex").Linter;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    var linter = Linter.init(allocator);
    defer linter.deinit();

    // Lint a pattern
    try linter.lint("(a+)+");

    // Print report
    const stdout = std.io.getStdOut().writer();
    try linter.printReport(stdout);
}

Detectable Issues

The linter can detect:

  1. Nested Quantifiers: Patterns like (a+)+ that can cause catastrophic backtracking
  2. Greedy Wildcards: .* and .+ that can be slow
  3. Empty Alternation Branches: a||b
  4. Unused Capture Groups: Groups that are never referenced
  5. Redundant Quantifiers: a?? or a**
  6. Anchors in Quantifiers: (^)+ which won't work as expected
  7. Very Large Repetitions: a{10000} that could cause issues
  8. Backtracking Risks: Complex patterns prone to exponential behavior

Example Output

Warnings (2):
  [WARNING] at position 0: Nested quantifiers detected - can cause catastrophic backtracking
    Suggestion: Consider simplifying the pattern or using atomic groups
  [WARNING] at position 0: Greedy wildcard quantifier (._ or .+) can be slow
    Suggestion: Consider using lazy quantifier (._? or .+?) or being more specific

Performance Features

Hybrid Engine Architecture

The library automatically chooses between two engines:

  • Thompson NFA: O(n*m) performance for basic patterns
  • Backtracking Engine: Supports advanced features (lazy quantifiers, lookahead, lookbehind, backreferences)
// This uses Thompson NFA (fast)
var regex1 = try Regex.compile(allocator, "\\d+[a-z]+");

// This uses backtracking (supports lazy quantifiers)
var regex2 = try Regex.compile(allocator, "a+?b");

// This uses backtracking (supports lookahead)
var regex3 = try Regex.compile(allocator, "foo(?=bar)");

Optimization Features

  • Literal Prefix Optimization: Patterns starting with literals use indexOf for fast scanning
  • Anchored Optimization: Patterns anchored at start/end avoid unnecessary scanning
  • Min/Max Length Calculation: Quickly reject inputs that are too short/long

Benchmarking

const benchmark = @import("regex").benchmark;

pub fn main() !void {
    var gpa = std.heap.GeneralPurposeAllocator(.{}){};
    defer _ = gpa.deinit();
    const allocator = gpa.allocator();

    try benchmark.runAllBenchmarks(allocator);
}

Thread Safety

The library provides thread-safe regex matching:

const thread_safety = @import("regex").thread_safety;

var regex = try Regex.compile(allocator, "\\d+");
defer regex.deinit();

// Thread-safe wrapper
var safe_regex = thread_safety.ThreadSafeRegex.init(&regex);

// Can be used from multiple threads
const result = try safe_regex.isMatch("123");

Profiling

Built-in profiling support:

const profiling = @import("regex").profiling;

var regex = try Regex.compile(allocator, "a+b+c+");
defer regex.deinit();

// Enable profiling
profiling.enable();

// Run matches
_ = try regex.isMatch("aaabbbccc");

// Get statistics
const stats = profiling.getStats();
std.debug.print("Matches: {d}, Total time: {d}ns\n", .{stats.match_count, stats.total_time_ns});

Summary

The zig-regex library provides:

  • ✅ Regex macros for pattern composition
  • ✅ Type-safe builder API
  • ✅ C FFI for language interoperability
  • ✅ WASM support for web environments
  • ✅ Lint and analysis tools
  • ✅ Hybrid engine (Thompson NFA + Backtracking)
  • ✅ Performance optimizations
  • ✅ Thread safety
  • ✅ Profiling support

All features are production-ready and fully tested with 175+ test cases passing.