//! A linear-time regular expression engine for operator rules (milestone-21
//! ruling 5): pattern → AST → NFA program → Pike VM.
//!
//! The engine exists because a filtering rule is written by the operator and
//! evaluated on every cache miss, so a pattern must never cost more than
//! O(program × name). A backtracking engine cannot promise that: `(a+)+b` takes
//! exponential time on a name of twenty `a`s. This VM simulates every NFA thread
//! in lockstep and admits each program counter at most once per input position,
//! which is what bounds the work.
//!
//! Syntax: literal bytes, `.`, character classes `[...]` with ranges and a
//! leading `^` for negation, the escapes `\d` and `\w` plus `\` before any other
//! ASCII punctuation to make it a literal, repetition `* + ? {n} {n,m} {n,}`,
//! alternation `|`, grouping `(...)`, and the anchors `^` and `$`. Matching is
//! unanchored unless an anchor is written, which is the POSIX-grep convention
//! Pi-hole users already expect. `.` matches any byte: a normalized name carries
//! no newline, so the usual newline exception would be a distinction without a
//! difference.
//!
//! Three places where a POSIX engine is more forgiving than this one, each so a
//! typo is reported rather than guessed at: a `]` inside a class is written
//! `\]` (the POSIX "a leading `]` is a member" rule is not honoured); a branch
//! may not be empty, so `ads|` is rejected rather than read as a pattern that
//! matches every name; and a quantifier may not be applied directly to another
//! quantifier, so `a+?` is rejected rather than read as `(a+)?`, which matches
//! every name. Parentheses say each of those deliberately.
//!
//! Refused, never approximated: backreferences, lookaround, captures, named
//! groups, Unicode classes, the `(?...)` prefix of all of them, and any escape
//! this file does not define (`\s`, `\b`, `\1`). Every one of those is
//! `error.BadPattern`.
//!
//! Groups do not capture. `(...)` is a grouping bracket and nothing else, so
//! there is no per-thread capture state and a thread is one program counter.
//!
//! Input is the normalized lowercase name, at most `types.max_name_len` bytes.
//! That bound is the caller's; nothing here depends on it.
//!
//! `std` is the only import: this file is the root of a separate fuzz module
//! (`tests/fuzz/regex_fuzz.zig`), and a module root cannot import across its own
//! directory boundary.

const std = @import("std");
const Allocator = std.mem.Allocator;

/// Ruling 5's two hard limits, each with its own error so an operator learns
/// which one they hit.
pub const max_pattern_len = 256;
pub const max_program_len = 1024;

pub const Error = error{ OutOfMemory, BadPattern, PatternTooLong, PatternTooComplex };

/// A program counter. `max_program_len` bounds every program, so a `u16` holds
/// any instruction index, any jump target and any thread-list slot.
pub const Pc = u16;

/// One byte per bit: a class is a 256-bit membership test, negation included, so
/// matching one input byte is one bit lookup no matter how the class was
/// written.
pub const Class = std.StaticBitSet(256);

pub const Inst = union(enum) {
    /// Consume one input byte equal to this one.
    byte: u8,
    /// Consume any one input byte.
    any,
    /// Consume one input byte the class at this index admits.
    class: u16,
    /// Two epsilon successors, both explored.
    split: Split,
    /// One epsilon successor.
    jump: Pc,
    /// Start of input.
    begin,
    /// End of input.
    end,
    /// Accept.
    match,

    pub const Split = struct { first: Pc, second: Pc };
};

/// A compiled pattern. Immutable once built: `matches` keeps its scratch on the
/// stack, so one program is safe to share across the query threads that evaluate
/// a filter snapshot concurrently.
///
/// Every program `compile` produces ends with `.match`, which is what lets the
/// VM step past a consuming instruction without a bounds check.
pub const Program = struct {
    insts: []const Inst = &.{},
    classes: []const Class = &.{},

    pub const empty: Program = .{};

    pub fn deinit(self: *Program, gpa: Allocator) void {
        gpa.free(self.insts);
        gpa.free(self.classes);
        self.* = .empty;
    }

    /// An exactly sized copy owned by `gpa`. `compile` takes its parse-time
    /// scratch — the AST, the child lists, the growing instruction buffer —
    /// from the allocator it builds the program with, so a caller whose
    /// long-lived allocator is an arena compiles into a scratch allocator and
    /// clones the finished program across. That keeps this file a
    /// single-allocator engine.
    pub fn clone(self: *const Program, gpa: Allocator) Allocator.Error!Program {
        const insts = try gpa.dupe(Inst, self.insts);
        errdefer gpa.free(insts);
        return .{ .insts = insts, .classes = try gpa.dupe(Class, self.classes) };
    }

    pub fn memoryBytes(self: *const Program) usize {
        return self.insts.len * @sizeOf(Inst) + self.classes.len * @sizeOf(Class);
    }
};

pub fn compile(gpa: Allocator, pattern: []const u8) Error!Program {
    if (pattern.len > max_pattern_len) return error.PatternTooLong;

    var parser: Parser = .{ .gpa = gpa, .src = pattern };
    defer parser.deinit();
    const root = try parser.parse();

    var emitter: Emitter = .{
        .gpa = gpa,
        .nodes = parser.nodes.items,
        .children = parser.children.items,
    };
    errdefer emitter.insts.deinit(gpa);
    try emitter.emitNode(root);
    _ = try emitter.add(.match);

    const insts = try emitter.insts.toOwnedSlice(gpa);
    errdefer gpa.free(insts);
    const classes = try parser.classes.toOwnedSlice(gpa);
    return .{ .insts = insts, .classes = classes };
}

pub fn matches(prog: *const Program, input: []const u8) bool {
    return run(prog, input).matched;
}

/// The verdict plus the work it took. `steps` is what the linearity claim is
/// tested against; production calls `matches` and drops it.
pub const Run = struct {
    matched: bool,
    /// One step is one program-counter admission. Each counter is admitted at
    /// most once per input position, so `steps` never exceeds
    /// `insts.len * (input.len + 1)`.
    steps: usize,
};

pub fn run(prog: *const Program, input: []const u8) Run {
    var vm: Vm = .{ .prog = prog, .input = input };
    return vm.execute();
}

// ---------------------------------------------------------------------------
// Parser: pattern text to AST
// ---------------------------------------------------------------------------

const Node = union(enum) {
    literal: u8,
    any,
    class: u16,
    begin,
    end,
    concat: Range,
    alternate: Range,
    repeat: Repeat,

    /// A window into the parser's shared child-index list.
    const Range = struct { start: u32, len: u32 };

    /// `max` is null for the unbounded forms `*`, `+` and `{n,}`.
    const Repeat = struct { child: u32, min: u32, max: ?u32 };
};

/// One class item: a single byte, or the set an escape such as `\d` stands for.
const Item = union(enum) { byte: u8, set: Class };

const digit_class: Class = classOf("0123456789");
const word_class: Class = classOf("0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ_");

fn classOf(comptime members: []const u8) Class {
    var set = Class.initEmpty();
    for (members) |c| set.set(c);
    return set;
}

const Parser = struct {
    gpa: Allocator,
    src: []const u8,
    pos: usize = 0,
    nodes: std.ArrayList(Node) = .empty,
    children: std.ArrayList(u32) = .empty,
    classes: std.ArrayList(Class) = .empty,

    fn deinit(self: *Parser) void {
        self.nodes.deinit(self.gpa);
        self.children.deinit(self.gpa);
        self.classes.deinit(self.gpa);
    }

    fn parse(self: *Parser) Error!u32 {
        if (self.src.len == 0) return error.BadPattern;
        const root = try self.parseAlternation();
        // Only an unbalanced ')' leaves input behind: every other construct
        // either consumes its bytes or fails.
        if (self.pos != self.src.len) return error.BadPattern;
        return root;
    }

    fn parseAlternation(self: *Parser) Error!u32 {
        var branches: std.ArrayList(u32) = .empty;
        defer branches.deinit(self.gpa);

        try branches.append(self.gpa, try self.parseConcat());
        while (self.at('|')) {
            self.pos += 1;
            try branches.append(self.gpa, try self.parseConcat());
        }
        if (branches.items.len == 1) return branches.items[0];
        return self.addNode(.{ .alternate = try self.addChildren(branches.items) });
    }

    fn parseConcat(self: *Parser) Error!u32 {
        var items: std.ArrayList(u32) = .empty;
        defer items.deinit(self.gpa);

        while (self.peek()) |c| {
            if (c == '|' or c == ')') break;
            try items.append(self.gpa, try self.parseRepeat());
        }
        // POSIX ERE holds that a branch carries at least one piece, and so does
        // this engine: an empty branch matches every name, and as a block rule
        // that is a sinkhole for the whole LAN.
        if (items.items.len == 0) return error.BadPattern;
        if (items.items.len == 1) return items.items[0];
        return self.addNode(.{ .concat = try self.addChildren(items.items) });
    }

    fn parseRepeat(self: *Parser) Error!u32 {
        const atom = try self.parseAtom();
        const repeated = (try self.parseQuantifier(atom)) orelse return atom;
        // A quantifier directly on another quantifier is refused. `a+?` is lazy
        // repetition in the engines that have one; this engine has none, so
        // reading it as `(a+)?` would compile a block rule that matches every
        // name. Parentheses say that deliberately, and `(a+)?` stays legal.
        if (self.peek()) |c| switch (c) {
            '*', '+', '?', '{' => return error.BadPattern,
            else => {},
        };
        return repeated;
    }

    /// Null when no quantifier follows, which leaves the atom as it stands.
    fn parseQuantifier(self: *Parser, child: u32) Error!?u32 {
        const c = self.peek() orelse return null;
        switch (c) {
            '*', '+', '?', '{' => self.pos += 1,
            else => return null,
        }
        return switch (c) {
            '*' => try self.addRepeat(child, 0, null),
            '+' => try self.addRepeat(child, 1, null),
            '?' => try self.addRepeat(child, 0, 1),
            '{' => try self.parseBrace(child),
            else => unreachable,
        };
    }

    fn parseAtom(self: *Parser) Error!u32 {
        const c = self.next() orelse return error.BadPattern;
        return switch (c) {
            '(' => blk: {
                // `(?` opens every construct this engine refuses: lookaround,
                // named groups, inline flags. Grouping is the only meaning here.
                if (self.at('?')) return error.BadPattern;
                const inner = try self.parseAlternation();
                if (!self.at(')')) return error.BadPattern;
                self.pos += 1;
                break :blk inner;
            },
            '[' => self.parseClass(),
            '.' => self.addNode(.any),
            '^' => self.addNode(.begin),
            '$' => self.addNode(.end),
            // A repetition operator with nothing in front of it.
            '*', '+', '?', '{' => error.BadPattern,
            '\\' => switch (try self.parseEscape()) {
                .byte => |b| self.addNode(.{ .literal = b }),
                .set => |s| self.addNode(.{ .class = try self.addClass(s) }),
            },
            else => self.addNode(.{ .literal = c }),
        };
    }

    /// The `\` is already consumed.
    fn parseEscape(self: *Parser) Error!Item {
        const c = self.next() orelse return error.BadPattern;
        if (c == 'd') return .{ .set = digit_class };
        if (c == 'w') return .{ .set = word_class };
        // Punctuation escapes to itself, which covers `\.`, `\\`, `\-` and the
        // metacharacters. A letter or digit would be a class or a backreference
        // this engine does not implement, and a high byte has no escape meaning.
        if (c >= 0x80 or std.ascii.isAlphanumeric(c)) return error.BadPattern;
        return .{ .byte = c };
    }

    /// The `[` is already consumed.
    fn parseClass(self: *Parser) Error!u32 {
        var set = Class.initEmpty();
        const negated = self.at('^');
        if (negated) self.pos += 1;

        var members: usize = 0;
        while (true) {
            const c = self.peek() orelse return error.BadPattern;
            if (c == ']') {
                self.pos += 1;
                break;
            }
            members += 1;
            switch (try self.classItem()) {
                .set => |s| set = set.unionWith(s),
                .byte => |lo| {
                    // `-` separates a range only between two single bytes, so
                    // `[-a]` and `[a-]` carry it as a member of the class.
                    if (!self.at('-') or self.pos + 1 >= self.src.len or self.src[self.pos + 1] == ']') {
                        set.set(lo);
                        continue;
                    }
                    self.pos += 1;
                    const hi = switch (try self.classItem()) {
                        .byte => |b| b,
                        // `[a-\d]` has no endpoint to range to.
                        .set => return error.BadPattern,
                    };
                    if (hi < lo) return error.BadPattern;
                    var b: usize = lo;
                    while (b <= hi) : (b += 1) set.set(b);
                },
            }
        }
        // `[]` and `[^]` are a class that admits nothing and a class that admits
        // everything; both are far more likely a typo than an intent.
        if (members == 0) return error.BadPattern;
        if (negated) set.toggleAll();
        return self.addNode(.{ .class = try self.addClass(set) });
    }

    fn classItem(self: *Parser) Error!Item {
        const c = self.next() orelse return error.BadPattern;
        if (c == '\\') return self.parseEscape();
        return .{ .byte = c };
    }

    /// The `{` is already consumed.
    fn parseBrace(self: *Parser, child: u32) Error!u32 {
        const min = try self.parseCount();
        var max: ?u32 = min;
        if (self.at(',')) {
            self.pos += 1;
            max = if (self.at('}')) null else try self.parseCount();
        }
        if (!self.at('}')) return error.BadPattern;
        self.pos += 1;
        if (max) |m| {
            if (m < min) return error.BadPattern;
        }
        return self.addRepeat(child, min, max);
    }

    fn parseCount(self: *Parser) Error!u32 {
        const start = self.pos;
        var value: u32 = 0;
        while (self.peek()) |c| {
            if (!std.ascii.isDigit(c)) break;
            self.pos += 1;
            // Capping before the multiply keeps the accumulator from wrapping,
            // and a count past the instruction budget can never yield a program.
            if (value > max_program_len) return error.PatternTooComplex;
            value = value * 10 + (c - '0');
        }
        if (self.pos == start) return error.BadPattern;
        if (value > max_program_len) return error.PatternTooComplex;
        return value;
    }

    fn peek(self: *const Parser) ?u8 {
        return if (self.pos < self.src.len) self.src[self.pos] else null;
    }

    fn at(self: *const Parser, c: u8) bool {
        return self.pos < self.src.len and self.src[self.pos] == c;
    }

    fn next(self: *Parser) ?u8 {
        if (self.pos >= self.src.len) return null;
        defer self.pos += 1;
        return self.src[self.pos];
    }

    fn addNode(self: *Parser, node: Node) Error!u32 {
        try self.nodes.append(self.gpa, node);
        return @intCast(self.nodes.items.len - 1);
    }

    fn addRepeat(self: *Parser, child: u32, min: u32, max: ?u32) Error!u32 {
        return self.addNode(.{ .repeat = .{ .child = child, .min = min, .max = max } });
    }

    fn addChildren(self: *Parser, items: []const u32) Error!Node.Range {
        const start = self.children.items.len;
        try self.children.appendSlice(self.gpa, items);
        return .{ .start = @intCast(start), .len = @intCast(items.len) };
    }

    fn addClass(self: *Parser, set: Class) Error!u16 {
        try self.classes.append(self.gpa, set);
        return @intCast(self.classes.items.len - 1);
    }
};

// ---------------------------------------------------------------------------
// Emitter: AST to NFA program
// ---------------------------------------------------------------------------

const Emitter = struct {
    gpa: Allocator,
    nodes: []const Node,
    children: []const u32,
    insts: std.ArrayList(Inst) = .empty,
    visits: usize = 0,

    /// A counted repetition re-emits its child, so emission is not bounded by
    /// the pattern length: `(){1000}` nested four deep asks for 10^12 visits of
    /// nodes that emit no instruction at all, which the instruction cap alone
    /// never notices. This budget is what makes compilation terminate. Any
    /// pattern that fits in `max_program_len` stays far below it.
    const max_visits = 1 << 16;

    fn add(self: *Emitter, inst: Inst) Error!Pc {
        if (self.insts.items.len >= max_program_len) return error.PatternTooComplex;
        try self.insts.append(self.gpa, inst);
        return @intCast(self.insts.items.len - 1);
    }

    fn here(self: *const Emitter) Pc {
        return @intCast(self.insts.items.len);
    }

    fn emitNode(self: *Emitter, index: u32) Error!void {
        self.visits += 1;
        if (self.visits > max_visits) return error.PatternTooComplex;

        switch (self.nodes[index]) {
            .literal => |b| _ = try self.add(.{ .byte = b }),
            .any => _ = try self.add(.any),
            .class => |c| _ = try self.add(.{ .class = c }),
            .begin => _ = try self.add(.begin),
            .end => _ = try self.add(.end),
            .concat => |r| for (self.children[r.start..][0..r.len]) |child| {
                try self.emitNode(child);
            },
            .alternate => |r| try self.emitAlternate(r),
            .repeat => |r| try self.emitRepeat(r),
        }
    }

    fn emitAlternate(self: *Emitter, range: Node.Range) Error!void {
        var jumps: std.ArrayList(Pc) = .empty;
        defer jumps.deinit(self.gpa);

        const branches = self.children[range.start..][0..range.len];
        for (branches, 0..) |branch, i| {
            if (i + 1 == branches.len) {
                try self.emitNode(branch);
                break;
            }
            const split = try self.add(.{ .split = .{ .first = 0, .second = 0 } });
            try self.emitNode(branch);
            try jumps.append(self.gpa, try self.add(.{ .jump = 0 }));
            self.insts.items[split] = .{ .split = .{ .first = split + 1, .second = self.here() } };
        }
        const end = self.here();
        for (jumps.items) |pc| self.insts.items[pc] = .{ .jump = end };
    }

    fn emitRepeat(self: *Emitter, repeat: Node.Repeat) Error!void {
        if (repeat.max) |max| {
            var i: u32 = 0;
            while (i < repeat.min) : (i += 1) try self.emitNode(repeat.child);

            // Each optional copy sits behind its own split, and every split
            // leaves for the same end, so stopping at copy `i` skips copies
            // `i` through `max`.
            var splits: std.ArrayList(Pc) = .empty;
            defer splits.deinit(self.gpa);
            while (i < max) : (i += 1) {
                const split = try self.add(.{ .split = .{ .first = 0, .second = 0 } });
                try splits.append(self.gpa, split);
                try self.emitNode(repeat.child);
            }
            const end = self.here();
            for (splits.items) |pc| {
                self.insts.items[pc] = .{ .split = .{ .first = pc + 1, .second = end } };
            }
            return;
        }

        if (repeat.min == 0) {
            const split = try self.add(.{ .split = .{ .first = 0, .second = 0 } });
            try self.emitNode(repeat.child);
            _ = try self.add(.{ .jump = split });
            self.insts.items[split] = .{ .split = .{ .first = split + 1, .second = self.here() } };
            return;
        }

        var i: u32 = 1;
        while (i < repeat.min) : (i += 1) try self.emitNode(repeat.child);
        const body = self.here();
        try self.emitNode(repeat.child);
        const split = try self.add(.{ .split = .{ .first = body, .second = 0 } });
        self.insts.items[split] = .{ .split = .{ .first = body, .second = split + 1 } };
    }
};

// ---------------------------------------------------------------------------
// Pike VM
// ---------------------------------------------------------------------------

/// The whole simulation state, and all of it on the caller's stack. Two reasons
/// it does not live in the `Program`:
///
///   - `matches` takes a `*const Program`, and a shared snapshot is evaluated
///     from several query threads at once, so scratch inside the program would
///     be a data race, not just a const violation;
///   - `max_program_len` is a compile-time constant, so a fixed array is exactly
///     as large as the largest program and needs no allocation at match time.
///
/// The cost is about 10 KiB of stack against a 16 MiB default thread stack, and
/// only the `marks` prefix a program actually uses is written on entry.
const Vm = struct {
    prog: *const Program,
    input: []const u8,
    /// The generation each program counter was last admitted under.
    marks: [max_program_len]u32 = undefined,
    /// The current and next thread lists; `current` alternates between them.
    lists: [2][max_program_len]Pc = undefined,
    lens: [2]usize = .{ 0, 0 },
    /// The epsilon-closure walk, iterative so a program of nested splits cannot
    /// grow the call stack. A counter enters it at most once per generation, so
    /// `max_program_len` slots are enough.
    stack: [max_program_len]Pc = undefined,
    gen: u32 = 0,
    steps: usize = 0,

    fn execute(self: *Vm) Run {
        const len = self.prog.insts.len;
        if (len == 0) return .{ .matched = false, .steps = 0 };
        @memset(self.marks[0..len], 0);

        var current: u1 = 0;
        self.gen = 1;
        self.lens[current] = 0;
        if (self.addThread(current, 0, 0)) return self.verdict(true);

        var pos: usize = 0;
        while (pos < self.input.len) {
            const byte = self.input[pos];
            const next: u1 = current ^ 1;
            self.gen += 1;
            self.lens[next] = 0;

            for (self.lists[current][0..self.lens[current]]) |pc| {
                const consumes = switch (self.prog.insts[pc]) {
                    .byte => |b| b == byte,
                    .any => true,
                    .class => |c| self.prog.classes[c].isSet(byte),
                    // Only a consuming instruction is ever filed into a list.
                    else => unreachable,
                };
                if (consumes and self.addThread(next, pc + 1, pos + 1)) return self.verdict(true);
            }

            pos += 1;
            // Unanchored by convention: every position starts a fresh attempt.
            // A pattern that opens with `^` fails this closure at once.
            if (self.addThread(next, 0, pos)) return self.verdict(true);
            current = next;
        }
        return self.verdict(false);
    }

    /// Walks the epsilon closure of `start` and files every consuming
    /// instruction it reaches into `list`. True when the closure reaches
    /// `.match`, which is the whole answer this engine reports: there are no
    /// captures to finish collecting and no longest-match rule to settle.
    fn addThread(self: *Vm, list: u1, start: Pc, pos: usize) bool {
        if (!self.admit(start)) return false;
        self.stack[0] = start;
        var top: usize = 1;

        while (top > 0) {
            top -= 1;
            const pc = self.stack[top];
            switch (self.prog.insts[pc]) {
                .jump => |target| self.push(&top, target),
                .split => |s| {
                    self.push(&top, s.second);
                    self.push(&top, s.first);
                },
                .begin => if (pos == 0) self.push(&top, pc + 1),
                .end => if (pos == self.input.len) self.push(&top, pc + 1),
                .match => return true,
                .byte, .any, .class => {
                    self.lists[list][self.lens[list]] = pc;
                    self.lens[list] += 1;
                },
            }
        }
        return false;
    }

    fn push(self: *Vm, top: *usize, pc: Pc) void {
        if (!self.admit(pc)) return;
        self.stack[top.*] = pc;
        top.* += 1;
    }

    /// The linearity invariant: a program counter is admitted at most once per
    /// generation, and there is one generation per input position, so no input
    /// can cost more than `insts.len * (input.len + 1)` steps.
    fn admit(self: *Vm, pc: Pc) bool {
        if (self.marks[pc] == self.gen) return false;
        self.marks[pc] = self.gen;
        self.steps += 1;
        return true;
    }

    fn verdict(self: *const Vm, matched: bool) Run {
        return .{ .matched = matched, .steps = self.steps };
    }
};

// ---------------------------------------------------------------------------
// tests
// ---------------------------------------------------------------------------

const testing = std.testing;

/// Every case asserts the step bound as well as the verdict, so linearity is
/// covered by the whole suite rather than by one test that watches a clock.
fn expectMatch(pattern: []const u8, input: []const u8, want: bool) !void {
    var prog = try compile(testing.allocator, pattern);
    defer prog.deinit(testing.allocator);

    const result = run(&prog, input);
    if (result.matched != want) {
        std.debug.print("pattern `{s}` against `{s}`: got {}, want {}\n", .{ pattern, input, result.matched, want });
        return error.TestUnexpectedResult;
    }
    try testing.expect(result.steps <= prog.insts.len * (input.len + 1));
    try testing.expectEqual(want, matches(&prog, input));
}

fn expectBad(comptime err: Error, pattern: []const u8) !void {
    try testing.expectError(err, compile(testing.allocator, pattern));
}

fn programLen(pattern: []const u8) !usize {
    var prog = try compile(testing.allocator, pattern);
    defer prog.deinit(testing.allocator);
    return prog.insts.len;
}

test "a literal pattern matches anywhere in the name" {
    try expectMatch("ads", "x.ads.example.com", true);
    try expectMatch("ads", "ads.example.com", true);
    try expectMatch("ads", "ad.example.com", false);
    try expectMatch("ads", "", false);
}

test "dot matches any one byte" {
    try expectMatch("a.c", "abc", true);
    try expectMatch("a.c", "a.c", true);
    try expectMatch("a.c", "ac", false);
}

test "an escaped dot is a literal dot" {
    try expectMatch("a\\.c", "a.c", true);
    try expectMatch("a\\.c", "abc", false);
}

test "punctuation escapes to itself" {
    try expectMatch("a\\\\b", "a\\b", true);
    try expectMatch("a\\-b", "a-b", true);
    try expectMatch("\\*", "a*b", true);
    try expectMatch("\\$", "a$", true);
}

test "a class matches any of its members" {
    try expectMatch("[abc]d", "bd", true);
    try expectMatch("[abc]d", "dd", false);
}

test "a class range covers its endpoints" {
    try expectMatch("[a-c]", "c", true);
    try expectMatch("[a-c]", "d", false);
    try expectMatch("ad[0-9]+-", "ad12-serve.example.com", true);
    try expectMatch("ad[0-9]+-", "ads-serve.example.com", false);
}

test "a negated class matches every byte outside it" {
    try expectMatch("[^.]x", "ax", true);
    try expectMatch("[^.]x", ".x", false);
    try expectMatch("^[^.]+$", "example", true);
    try expectMatch("^[^.]+$", "example.com", false);
}

test "a hyphen at either end of a class is a member" {
    try expectMatch("[-a]", "-", true);
    try expectMatch("[a-]", "-", true);
    try expectMatch("[a-]", "a", true);
    try expectMatch("[a-]", "b", false);
}

test "an escape inside a class carries its set" {
    try expectMatch("[\\d]", "7", true);
    try expectMatch("[\\d]", "a", false);
    try expectMatch("[\\dx]", "x", true);
    try expectMatch("[\\]]", "]", true);
}

test "digit and word escapes" {
    try expectMatch("\\d\\d", "ad42.example.com", true);
    try expectMatch("\\d\\d", "ad4.example.com", false);
    try expectMatch("^\\w+$", "ads_42", true);
    try expectMatch("^\\w+$", "ads.42", false);
}

test "star matches zero or more" {
    try expectMatch("^ab*c$", "ac", true);
    try expectMatch("^ab*c$", "abbbc", true);
    try expectMatch("^ab*c$", "adc", false);
}

test "plus matches one or more" {
    try expectMatch("^ab+c$", "ac", false);
    try expectMatch("^ab+c$", "abc", true);
    try expectMatch("^ab+c$", "abbbc", true);
}

test "question mark matches zero or one" {
    try expectMatch("^ads?\\.example$", "ad.example", true);
    try expectMatch("^ads?\\.example$", "ads.example", true);
    try expectMatch("^ads?\\.example$", "adss.example", false);
}

test "an exact count repeats exactly" {
    try expectMatch("^a{3}$", "aaa", true);
    try expectMatch("^a{3}$", "aa", false);
    try expectMatch("^a{3}$", "aaaa", false);
}

test "a bounded count repeats within its bounds" {
    try expectMatch("^a{2,4}$", "a", false);
    try expectMatch("^a{2,4}$", "aa", true);
    try expectMatch("^a{2,4}$", "aaaa", true);
    try expectMatch("^a{2,4}$", "aaaaa", false);
}

test "an open count repeats without an upper bound" {
    try expectMatch("^a{2,}$", "a", false);
    try expectMatch("^a{2,}$", "aa", true);
    try expectMatch("^a{2,}$", "aaaaaaaa", true);
    // `{n,}` lowers to `n` copies followed by a star, so it is `{n}` in its
    // lower bound and unbounded above, in the middle of a pattern as well.
    try expectMatch("^ab{2,}c$", "abc", false);
    try expectMatch("^ab{2,}c$", "abbc", true);
    try expectMatch("^ab{2,}c$", "abbbbbc", true);
    try expectMatch("^a{0,}$", "", true);
    try expectMatch("^a{0,}$", "aaa", true);
    try expectMatch("^a{0,}$", "b", false);
    try expectMatch("^(ab){2,}$", "abab", true);
    try expectMatch("^(ab){2,}$", "ab", false);
}

test "a zero count emits nothing" {
    try expectMatch("^a{0}b$", "b", true);
    try expectMatch("^a{0}b$", "ab", false);
    try expectMatch("^a{0,0}b$", "b", true);
    try expectMatch("^a{0,0}b$", "ab", false);
    // `{0}` around a group leaves the group out of the program entirely.
    try testing.expectEqual(@as(usize, 2), try programLen("^(abc){0}"));
}

test "alternation takes either branch" {
    try expectMatch("^(ads|track)\\.example$", "ads.example", true);
    try expectMatch("^(ads|track)\\.example$", "track.example", true);
    try expectMatch("^(ads|track)\\.example$", "other.example", false);
}

test "alternation of three branches" {
    try expectMatch("^(a|b|c)$", "a", true);
    try expectMatch("^(a|b|c)$", "b", true);
    try expectMatch("^(a|b|c)$", "c", true);
    try expectMatch("^(a|b|c)$", "d", false);
}

test "a group repeats as a unit" {
    try expectMatch("^(ab)+$", "abab", true);
    try expectMatch("^(ab)+$", "aba", false);
    try expectMatch("^(ab|cd){2}$", "abcd", true);
    try expectMatch("^(ab|cd){2}$", "ab", false);
}

test "the start anchor pins a match to the first byte" {
    try expectMatch("^ads", "ads.example.com", true);
    try expectMatch("^ads", "x.ads.example.com", false);
}

test "the end anchor pins a match to the last byte" {
    try expectMatch("example$", "x.example", true);
    try expectMatch("example$", "example.com", false);
}

test "both anchors demand the whole name" {
    try expectMatch("^ads\\.example\\.com$", "ads.example.com", true);
    try expectMatch("^ads\\.example\\.com$", "www.ads.example.com", false);
    try expectMatch("^$", "", true);
    try expectMatch("^$", "a", false);
}

test "an unanchored pattern matches at any offset" {
    try expectMatch("doubleclick", "static.doubleclick.net", true);
    try expectMatch("doubleclick", "doubleclick", true);
    try expectMatch("doubleclick", "doubleclic", false);
}

test "an unterminated group is rejected" {
    try expectBad(error.BadPattern, "(");
    try expectBad(error.BadPattern, "(ab");
    try expectBad(error.BadPattern, "(ab|cd");
}

test "an unbalanced closing paren is rejected" {
    try expectBad(error.BadPattern, ")");
    try expectBad(error.BadPattern, "ab)");
}

test "a reversed class range is rejected" {
    try expectBad(error.BadPattern, "[z-a]");
    try expectBad(error.BadPattern, "[9-0]");
}

test "an unterminated class is rejected" {
    try expectBad(error.BadPattern, "[");
    try expectBad(error.BadPattern, "[abc");
    try expectBad(error.BadPattern, "[a-");
}

test "an empty class is rejected" {
    try expectBad(error.BadPattern, "[]");
    try expectBad(error.BadPattern, "[^]");
}

test "a class range against a set escape is rejected" {
    try expectBad(error.BadPattern, "[a-\\d]");
}

test "a trailing backslash is rejected" {
    try expectBad(error.BadPattern, "ads\\");
    try expectBad(error.BadPattern, "\\");
    try expectBad(error.BadPattern, "[a\\");
}

test "a reversed repetition bound is rejected" {
    try expectBad(error.BadPattern, "a{2,1}");
}

test "a malformed repetition is rejected" {
    try expectBad(error.BadPattern, "a{}");
    try expectBad(error.BadPattern, "a{,3}");
    try expectBad(error.BadPattern, "a{2");
    try expectBad(error.BadPattern, "a{2,3");
    try expectBad(error.BadPattern, "a{x}");
}

test "a quantifier applied to a quantifier is rejected" {
    // `a+?` is lazy repetition in engines that have one. This engine has none,
    // so reading it as `(a+)?` would compile a block rule that matches every
    // name — the failure the empty-branch rejection also guards against, and
    // worse here because the pattern looks like it worked.
    try expectBad(error.BadPattern, "a+?");
    try expectBad(error.BadPattern, "a*?");
    try expectBad(error.BadPattern, "a??");
    try expectBad(error.BadPattern, "a{2,3}?");
    try expectBad(error.BadPattern, "a++");
    try expectBad(error.BadPattern, "a*+");
    try expectBad(error.BadPattern, "a**");
    try expectBad(error.BadPattern, "a{2}{3}");
    try expectBad(error.BadPattern, "[a-c]+*");
}

test "parentheses say a repeated repetition deliberately" {
    try expectMatch("(a+)?", "zzz", true);
    try expectMatch("^(a+)?$", "aaa", true);
    try expectMatch("^(a+)?$", "", true);
    try expectMatch("^(a+)?$", "b", false);
    try expectMatch("^(a{2}){3}$", "aaaaaa", true);
    try expectMatch("^(a{2}){3}$", "aaaa", false);
}

test "a repetition operator with nothing to repeat is rejected" {
    try expectBad(error.BadPattern, "*ads");
    try expectBad(error.BadPattern, "+ads");
    try expectBad(error.BadPattern, "?ads");
    try expectBad(error.BadPattern, "{2}");
    try expectBad(error.BadPattern, "(*)");
}

test "an empty pattern or an empty branch is rejected" {
    try expectBad(error.BadPattern, "");
    try expectBad(error.BadPattern, "a|");
    try expectBad(error.BadPattern, "|a");
    try expectBad(error.BadPattern, "()");
    try expectBad(error.BadPattern, "a||b");
}

test "the constructs this engine refuses are rejected, never approximated" {
    // Lookaround, non-capturing and named group syntax, inline flags.
    try expectBad(error.BadPattern, "(?:ab)");
    try expectBad(error.BadPattern, "(?=ab)");
    try expectBad(error.BadPattern, "(?!ab)");
    try expectBad(error.BadPattern, "(?<name>ab)");
    try expectBad(error.BadPattern, "(?i)ab");
    // A backreference and the escapes that are not defined here.
    try expectBad(error.BadPattern, "(a)\\1");
    try expectBad(error.BadPattern, "a\\s");
    try expectBad(error.BadPattern, "a\\b");
    try expectBad(error.BadPattern, "\\D");
    try expectBad(error.BadPattern, "\\W");
    try expectBad(error.BadPattern, "\\p{L}");
}

test "an over-long pattern is rejected before it is parsed" {
    const pattern = "a" ** (max_pattern_len + 1);
    try expectBad(error.PatternTooLong, pattern);
    // The bound itself still compiles.
    try testing.expectEqual(@as(usize, max_pattern_len + 1), try programLen("a" ** max_pattern_len));
}

test "a pattern past the instruction budget is rejected" {
    try expectBad(error.PatternTooComplex, "(abcd){400}");
    try expectBad(error.PatternTooComplex, "(ab|cd){0,400}");
}

test "a repetition count past the instruction budget is rejected" {
    try expectBad(error.PatternTooComplex, "a{2000}");
    try expectBad(error.PatternTooComplex, "a{0,99999999999999999999}");
}

test "nested counted repetition of an empty body terminates" {
    // Nothing here emits an instruction, so the instruction cap never fires;
    // the emitter's visit budget is the only thing that ends this compile.
    try expectBad(error.PatternTooComplex, "((((x{0}){900}){900}){900}){900}");
}

test "the classic backtracker killer stays inside the step bound" {
    const input = "a" ** 20 ++ "X";
    var prog = try compile(testing.allocator, "(a+)+b");
    defer prog.deinit(testing.allocator);

    const result = run(&prog, input);
    try testing.expect(!result.matched);
    try testing.expect(result.steps <= prog.insts.len * (input.len + 1));
    // A backtracker explores 2^20 paths here; this program is five
    // instructions over twenty-one positions.
    try testing.expect(result.steps < 256);
}

test "nested alternation stays inside the step bound" {
    const input = "a" ** 40 ++ "b";
    var prog = try compile(testing.allocator, "^(a|aa)+$");
    defer prog.deinit(testing.allocator);

    const result = run(&prog, input);
    try testing.expect(!result.matched);
    try testing.expect(result.steps <= prog.insts.len * (input.len + 1));
}

test "a pathological pattern over a full-length name stays inside the step bound" {
    // 253 bytes is the longest text name, which is the worst case in production.
    const input = "a" ** 252 ++ "X";
    var prog = try compile(testing.allocator, "^(a*)*(b*)*$");
    defer prog.deinit(testing.allocator);

    const result = run(&prog, input);
    try testing.expect(!result.matched);
    try testing.expect(result.steps <= prog.insts.len * (input.len + 1));
}

test "a loop whose body consumes nothing terminates" {
    // `(a{0})+` and `((a*)*)*` compile to an epsilon cycle. Nothing but the
    // one-admission-per-position rule ends the walk over it.
    try expectMatch("(a{0})+", "zzz", true);
    try expectMatch("^(a{0})+$", "zzz", false);
    try expectMatch("^((a*)*)*$", "aaa", true);
    try expectMatch("^((a*)*)*$", "aab", false);
    try expectMatch("(a?)*b", "aaab", true);
    try expectMatch("(a?)*b", "aaac", false);
}

test "matching a const program needs no mutable state" {
    const prog = try compile(testing.allocator, "^ads?\\.");
    defer {
        var owned = prog;
        owned.deinit(testing.allocator);
    }
    try testing.expect(matches(&prog, "ads.example.com"));
    try testing.expect(matches(&prog, "ad.example.com"));
    try testing.expect(!matches(&prog, "www.ads.example.com"));
}

test "matching is deterministic" {
    var prog = try compile(testing.allocator, "^(ad|track)[0-9]{1,3}\\.example\\.(com|net)$");
    defer prog.deinit(testing.allocator);

    const first = run(&prog, "track42.example.net");
    const second = run(&prog, "track42.example.net");
    try testing.expectEqual(first.matched, second.matched);
    try testing.expectEqual(first.steps, second.steps);
    try testing.expect(first.matched);
}

test "an empty program never matches" {
    var prog: Program = .empty;
    try testing.expect(!matches(&prog, "ads.example.com"));
    try testing.expectEqual(@as(usize, 0), prog.memoryBytes());
    prog.deinit(testing.allocator);
}

test "memoryBytes counts instructions and classes" {
    var plain = try compile(testing.allocator, "^abc$");
    defer plain.deinit(testing.allocator);
    var classed = try compile(testing.allocator, "^[abc]$");
    defer classed.deinit(testing.allocator);

    try testing.expect(plain.memoryBytes() > 0);
    try testing.expect(classed.memoryBytes() > plain.memoryBytes() - @sizeOf(Inst));
    try testing.expectEqual(@as(usize, 0), plain.classes.len);
    try testing.expectEqual(@as(usize, 1), classed.classes.len);
}

test "deinit leaves a program that owns nothing" {
    var prog = try compile(testing.allocator, "^ads$");
    prog.deinit(testing.allocator);
    try testing.expectEqual(@as(usize, 0), prog.insts.len);
    try testing.expect(!matches(&prog, "ads"));
    prog.deinit(testing.allocator);
}

test "a clone matches what the original matched and outlives it" {
    var scratch: std.heap.ArenaAllocator = .init(testing.allocator);
    var original = try compile(scratch.allocator(), "^(ad|track)[0-9]{2,5}\\.example\\.(com|net)$");
    var copy = try original.clone(testing.allocator);
    defer copy.deinit(testing.allocator);
    scratch.deinit();

    try testing.expectEqual(original.insts.len, copy.insts.len);
    try testing.expectEqual(original.classes.len, copy.classes.len);
    try testing.expectEqual(original.memoryBytes(), copy.memoryBytes());
    try testing.expect(matches(&copy, "ad4242.example.com"));
    try testing.expect(!matches(&copy, "ad4242.example.org"));
}

fn cloneUnderFailure(gpa: Allocator) !void {
    var original = try compile(std.testing.allocator, "^[a-c]d(x|y)$");
    defer original.deinit(std.testing.allocator);
    var copy = try original.clone(gpa);
    defer copy.deinit(gpa);
    try testing.expect(matches(&copy, "adx"));
}

test "clone under allocation failure" {
    try testing.checkAllAllocationFailures(testing.allocator, cloneUnderFailure, .{});
}

fn compileUnderFailure(gpa: Allocator) !void {
    var prog = try compile(gpa, "^(ad|track)[0-9]{2,5}\\.example\\.(com|net)$");
    defer prog.deinit(gpa);
    try testing.expect(matches(&prog, "ad4242.example.com"));
}

test "compile under allocation failure" {
    try testing.checkAllAllocationFailures(testing.allocator, compileUnderFailure, .{});
}
