LibRegex: Preserve set-op direction in backward /v matches

Unicode-set intersection and subtraction always lowered their
post-consumption checks as lookbehinds. That is correct while the
outer matcher runs forward, but inside lookbehind the consumed text
sits to the right of the current position, so the checks must flip
to lookahead instead. Because we always looked left, patterns like
`(?<=[[^A-Z]--[A-Z]])P{N}` and the reported fuzz case missed
matches whenever the character before the consumed one changed the
set-operation result.

Preserve the surrounding match direction when compiling those
checks, and add coverage for reduced subtraction and intersection
cases plus the original regression.
This commit is contained in:
Andreas Kling 2026-03-30 19:39:25 +02:00 committed by Andreas Kling
parent e0de4ef33e
commit 201e615aad
2 changed files with 28 additions and 6 deletions

View file

@ -1106,20 +1106,24 @@ impl Compiler {
self.emit(Instruction::Fail);
return;
}
// NB: In backward mode the already-consumed text sits to
// the right of the current position, so these checks need
// to flip from lookbehind to lookahead.
let forward = self.backward;
let look_start = self.emit(Instruction::LookStart {
positive: true,
forward: false,
forward,
end: u32::MAX,
});
let saved_backward = self.backward;
self.backward = true;
self.backward = !forward;
self.compile_class_set_operand_at_length(operand, length);
self.backward = saved_backward;
self.emit(Instruction::LookEnd);
let end = self.current_offset();
self.program.instructions[look_start as usize] = Instruction::LookStart {
positive: true,
forward: false,
forward,
end,
};
}
@ -1134,20 +1138,21 @@ impl Compiler {
if !self.class_set_operand_lengths(operand).contains(&length) {
continue;
}
let forward = self.backward;
let look_start = self.emit(Instruction::LookStart {
positive: false,
forward: false,
forward,
end: u32::MAX,
});
let saved_backward = self.backward;
self.backward = true;
self.backward = !forward;
self.compile_class_set_operand_at_length(operand, length);
self.backward = saved_backward;
self.emit(Instruction::LookEnd);
let end = self.current_offset();
self.program.instructions[look_start as usize] = Instruction::LookStart {
positive: false,
forward: false,
forward,
end,
};
}

View file

@ -253,6 +253,23 @@ test("global unicode matches keep low-surrogate empty matches that V8 finds", ()
expect(subject.match(/\p{Script=Cyrillic}?(?<!\D)/gv)).toEqual(new Array(24).fill(""));
});
test("backward v-mode class-set operations inspect the consumed code point", () => {
expect("A n".match(/(?<=[[^A-Z]--[A-Z]])\P{N}/gv)).toEqual(["n"]);
expect("A n".match(/(?<=[[^0-9]&&[^A-Z]])\P{N}/gv)).toEqual(["n"]);
expect("В Β".match(/(?<=[[^А-Я]--[А-Я]])\P{N}/gv)).toEqual(["Β"]);
expect("Дnह".match(/(?<=[[^А-Я]--[А-Я]])\P{N}/gv)).toEqual([""]);
const subject =
"🤔🤔🤔🤔🤔 💫🎊✨🎈🎀Γ 5 208549 😂ש ∂∂∂∂ В ΒβιηδγΓ\\nYזااااا1טתזעוש M αα שלום 8🔤😐 P¥~μμμ سمвшДnहर`7*️⃣*️⃣*️⃣ 🙃🙃🙃🙃🙃привет";
const matches = subject.match(/(?<=[[^А-Я]--[А-Я]])(\P{N})/gv);
const positions = Array.from(subject.matchAll(/(?<=[[^А-Я]--[А-Я]])(\P{N})/gv), match => match.index);
expect(matches).not.toBeNull();
expect(matches.length).toBe(95);
expect(positions.includes(subject.indexOf("Β"))).toBeTrue();
expect(positions.includes(subject.indexOf("ह"))).toBeTrue();
});
test("regexp object as pattern parameter", () => {
expect(RegExp(/foo/).toString()).toBe("/foo/");
expect(RegExp(/foo/g).toString()).toBe("/foo/g");