diff --git a/CLAUDE.md b/CLAUDE.md index 0dc37de..5925d51 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -31,8 +31,9 @@ shell provides `zig_0_16`, `qemu` (for the aarch64 tests), `reuse`, and `-- N seed` (generated sweep), `--corpus /testdata` (PCRE2's own test patterns), `--corpus-mutate N seed` (seeded edits of those), `--case '' '' [ims]` and `--engines '' ''` (single-case - reducers for shrinking). Each compares three ways: leftmost match, every - match, and searches from an offset. + reducers for shrinking). The generated sweep compares three ways: leftmost + match, every match, and searches from an offset; the corpus and + corpus-mutate modes compare the first two. - `zig build bench-run -- --builtin` — benchmark against an in-memory corpus. `./bench/run.sh` runs the full comparative suite (PCRE2, Python, Perl, POSIX). - `zig fmt --check build.zig src bench tools` and `reuse lint` — CI gates. diff --git a/src/compiler.zig b/src/compiler.zig index 4712628..fdd026b 100644 --- a/src/compiler.zig +++ b/src/compiler.zig @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT -//! Compiles the parser's AST into bytecode shared by both engines. +//! Compiles the parser's AST into bytecode shared by all four engines. //! //! The compiler runs in two modes over identical code paths: a counting pass //! that only measures (`counting = true`), and an emitting pass that writes @@ -35,12 +35,6 @@ pub const BackrefOp = struct { ci: bool, }; -/// Fused single-element repeat, emitted only into backtracker programs -/// (`for_backtrack`). The repeated element is the NEXT instruction (a char, -/// class, or any), keeping `Inst` small; execution continues at pc + 2. The -/// interpreter consumes greedily in one tight loop and backtracking retries -/// are a single range frame instead of one frame per iteration. The Pike VM -/// and DFA never see this instruction. /// An iteration that consumed nothing ends the loop — it does not fail. The /// distinction is observable: `(a*)*` against "aa" must report the final /// empty iteration's capture, and `(a*?)*` must match empty rather than be @@ -51,6 +45,12 @@ pub const ExitIfSame = struct { target: u32, }; +/// Fused single-element repeat, emitted only into backtracker programs +/// (`for_backtrack`). The repeated element is the NEXT instruction (a char, +/// class, or any), keeping `Inst` small; execution continues at pc + 2. The +/// interpreter consumes greedily in one tight loop and backtracking retries +/// are a single range frame instead of one frame per iteration. The Pike VM +/// and DFA never see this instruction. pub const RepOp = struct { min: u32, /// `unbounded` (maxInt) means no upper bound. diff --git a/src/dfa.zig b/src/dfa.zig index d2c4889..54f8642 100644 --- a/src/dfa.zig +++ b/src/dfa.zig @@ -45,7 +45,8 @@ const Entry = struct { next: u32, /// Closure at the boundary before consuming contained Match. matched_here: bool, - /// No pre-seed thread survived the step: threads all died. + /// No thread survived the step — every thread died, this position's + /// freshly-seeded one included. stepped_empty: bool, }; @@ -118,7 +119,7 @@ pub const Machine = struct { /// Cache ceiling; reaching it makes the search give up and hand the /// subject to the Pike VM. Settable so that a test can pin it low /// enough to take that path on ordinary patterns, which at the - /// default of five hundred states almost nothing does. + /// default of 512 states almost nothing does. max_states: usize, ) std.mem.Allocator.Error!Machine { const n = prog.insts.len; @@ -132,7 +133,7 @@ pub const Machine = struct { // literal copies arena_state, so it must already know these buffers. // Each cached state's key is up to four bytes per instruction, so a // program near the instruction ceiling would put the default cap of - // five hundred states at over a hundred megabytes of keys. Scale the + // 512 states at over a hundred megabytes of keys. Scale the // cap down so the key store stays within a few megabytes; giving up // earlier just hands the subject to the Pike VM sooner. const scaled_max = @min(max_states, @max(8, (4 << 20) / (4 * n + 1))); diff --git a/src/fuzz.zig b/src/fuzz.zig index b5d927e..730af53 100644 --- a/src/fuzz.zig +++ b/src/fuzz.zig @@ -85,7 +85,7 @@ const configs = [_]Config{ .{ // The lazy DFA gives up when its state cache fills and hands the // subject to the Pike VM, which must reach the same answer. At the - // real ceiling of five hundred states almost no pattern gets there, + // real ceiling of 512 states almost no pattern gets there, // so this pins it low enough that most of them do. .name = "dfa-tiny-cache", .handles_backtrack_only = false, diff --git a/src/jit/codegen_x64.zig b/src/jit/codegen_x64.zig index 83b5e38..c838bba 100644 --- a/src/jit/codegen_x64.zig +++ b/src/jit/codegen_x64.zig @@ -124,8 +124,9 @@ const Blob = struct { len: u8, }; -/// Widest run scanner: two SSE registers hold each range's bounds, leaving -/// four for the data, the accumulator, and two temporaries. +/// Deduplicates emitted bit-table blobs by content, so a class whose table +/// was already emitted reuses that label instead of emitting it again (see +/// `emitBlob`). const BitsCache = struct { bytes: [32]u8, len: u8, @@ -166,7 +167,8 @@ pub const Gen = struct { return l; } - /// Consume a run of accepted ASCII bytes sixteen at a time. + /// Consume a run of accepted ASCII bytes sixteen at a time (thirty-two + /// under AVX2). /// /// For each range the vector `(lo > v) | (v > hi)` marks the bytes /// outside it; ANDing those across ranges marks bytes outside the whole @@ -174,7 +176,7 @@ pub const Gen = struct { /// so any byte >= 0x80 reads as negative, falls outside every ASCII /// range, and correctly stops the scan — the scalar loop that follows /// then handles it. Exact for ASCII-only children, so the scalar loop - /// only ever finishes the sub-16-byte tail. + /// only ever finishes the sub-block tail. fn emitSimdRun(self: *Gen, set: AsciiSet) Error!void { // AVX2 doubles the block and, being three-operand, drops the register // copies the SSE2 form needs before each compare. @@ -731,9 +733,6 @@ pub const Gen = struct { } }; -/// A literal the continuation of a greedy repeat must match next, letting -/// retries jump between its occurrences (the JIT's form of the interpreter's -/// required-literal scanning). /// The widest run scanner this program will emit, in ranges: that decides how /// many vector registers the code touches, and so how many Windows expects it /// to hand back unchanged. diff --git a/src/jit/runtime.zig b/src/jit/runtime.zig index d35d012..5bba065 100644 --- a/src/jit/runtime.zig +++ b/src/jit/runtime.zig @@ -232,8 +232,6 @@ pub const AsciiSet = struct { len: usize, }; -/// Identical classes appear once per emit site (a repeat emits its child -/// several times); one table each is enough. /// The ASCII acceptance set of a repeat's child, as ranges, when it is /// narrow enough to drive the vector scanner. Null when the instruction /// can match a non-ASCII codepoint (the scanner would have to stop and diff --git a/src/tests.zig b/src/tests.zig index e4f5639..1ec9754 100644 --- a/src/tests.zig +++ b/src/tests.zig @@ -1,7 +1,7 @@ // SPDX-FileCopyrightText: © 2026 Jeffrey C. Ollie // SPDX-License-Identifier: MIT -//! Behavior tests exercising the full pipeline through both engines. +//! Behavior tests exercising the full pipeline through all four engines. const std = @import("std"); const zregex = @import("root.zig"); const Regex = zregex.Regex; @@ -918,8 +918,8 @@ test "a variable-length lookbehind takes the longest match" { test "iteration retries for a longer match where it found an empty one" { // After an empty match, the next thing to try is a longer match in the // same place -- not the next position. `.*?` reports empty at 0 and then - // all of `ab`; advancing straight away reported empty at 0, 1 and 2 and - // never the text between them. PCRE does this (it is the loop pcre2demo.c + // `a` and `b` in turn; advancing straight away reported empty at 0, 1 and 2 + // and never the text between them. PCRE does this (it is the loop pcre2demo.c // documents) and so does Python; zregex was the odd one out, which only // showed once the oracle started comparing every match rather than the // leftmost one. diff --git a/tools/oracle.zig b/tools/oracle.zig index 290528b..6949afb 100644 --- a/tools/oracle.zig +++ b/tools/oracle.zig @@ -10,9 +10,13 @@ //! generated patterns through PCRE2 and compares. //! //! Needs PCRE2 to link, so it is a separate tool rather than part of -//! `zig build test`: +//! `zig build test`; it builds PCRE2 from source itself, so nothing need be +//! installed: //! -//! zig build oracle -Dpcre2-include= -Dpcre2-lib= -- [cases] [seed] +//! zig build oracle -- [cases] [seed] +//! +//! To link a different PCRE2 build instead, override with +//! `-Dpcre2-include= -Dpcre2-lib=`. //! //! Comparisons are deliberately narrowed to where both engines claim the same //! semantics: @@ -872,8 +876,8 @@ fn runCorpus(gpa: std.mem.Allocator, io: std.Io, dir_path: []const u8, verbose: /// nudged, two branches spliced from different files. Those neighborhoods /// are where an off-by-one in the parser or compiler lives, and neither /// source visits them. This mode does: it loads every corpus pattern, applies -/// a few seeded mutations, and holds the result to PCRE2 the same three ways -/// as the sweep -- leftmost match, every match, and from an offset. +/// a few seeded mutations, and holds the result to PCRE2 the same two ways the +/// corpus run does -- leftmost match and every match. /// /// zig build oracle ... -- --corpus-mutate /testdata [cases] [seed] fn runCorpusMutate(