Skip to content

[BUG] RegexSearchStrategy: $ and \b / \B can false-match on a chunk-boundary illusion at the tail of a chunk #49

Description

@TomStrepsil

Describe the bug

Same family of bug as the companion start-boundary issue, opposite edge. completeMatchRegex.exec() runs against remainingHaystack (search-strategy.ts:67-68)— a slice that simply runs out wherever the current chunk happens to end.

Native $ (no m) and \b/\B at the tail evaluate "is this the end of input" relative to the slice's end, not the true stream's end. The strategy cannot know, mid-stream, whether more chunks are coming — but the regex engine, given only the truncated slice, confidently treats "no more characters in this string" as "true end of input."

Unlike the start-boundary case, this side is not decidable from information already in hand — the correctness of $/\b/\B here depends on the next character, which by definition hasn't arrived yet when a chunk boundary lands mid-pattern. Resolving it correctly needs either (a) the actual next character, or (b) confirmation the stream has truly ended — and (b) only happens at flush(), which today just returns the leftover buffer as raw text and never re-runs completeMatchRegex (see "Additional context" below).

Currently undocumented — README lists boundary assertions as fully "✅ Supported."

To Reproduce

import { RegexSearchStrategy } from "./src/search-strategies/regex/search-strategy.ts";

const strategy = new RegexSearchStrategy(/foo\b/);
const state = strategy.createState();
for (const chunk of ["foo", "bar"]) {
  for (const r of strategy.processChunk(chunk, state)) console.log(r);
}

Actual output:

{ isMatch: true, content: [ "foo" ] }   // <-- WRONG
{ isMatch: false, content: "bar" }

Same defect reproduces for /foo$/ (no m) over ["foo", "bar"]$ spuriously succeeds at the end of chunk 1 because the slice "foo" happens to run out there, not because the stream has actually ended.

Expected behaviour

No match. True (unchunked) string is "foobar"; there is no word boundary between "foo" and "bar" (both word characters), so /foo\b/ does not match anywhere in it.

Additional context

  • Why this one is harder to fix than the companion start-boundary bug: a correct fix requires deferring the match until either more input arrives (to re-resolve with a real next character) or the stream is confirmed finished. The latter only happens at flush() — but SearchStrategy.flush(state): string (types.ts) is currently a fixed part of the strategy interface, always treated as raw leftover text by every call site (transform-engine-base.ts:40,47, engine.ts:212,248), never re-run through completeMatchRegex. Deferring these matches in processChunk without also teaching flush() to resolve them would trade today's false positives for new false negatives (a genuinely final /foo$/ match would buffer forever and get dumped as unmatched text). A full fix therefore touches the SearchStrategy interface contract, not just RegexSearchStrategy.
  • This is architecturally the same shape as the already-documented "Unbounded Quantifiers" limitation in the README (a greedy quantifier is satisfied eagerly at whatever the chunk boundary happens to be) — except quantifiers are a documented, accepted trade-off, and $/\b/\B currently are not.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't working

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions