Skip to content

runtime: route RegExp.test through fancy-regex fallback for lookbehind patterns #2723

Description

@andrewtdiz

Summary

Perry has a partial fancy_regex fallback for JavaScript RegExp features that Rust's regex crate rejects, including lookbehind assertions. The fallback is used by String.prototype.match() and RegExp.prototype.exec(), but RegExp.prototype.test() still calls the placeholder Rust regex directly, so lookbehind tests return false even when Node returns true.

This leaves Node-compatible packages broken when they use lookbehind through .test(), including dynamic receiver cases that dispatch through the same runtime helper.

Expected Node behavior

On current Node:

/(?<=@)\w+/.test("user@example")        // true
/(?<!@)\w+/.test("user@example")        // true
/(?<=@)\w+/.exec("user@example")?.[0]   // "example"
"user@example".match(/(?<=@)\w+/)?.[0] // "example"

Current Perry evidence

The current runtime already detects unsupported regex patterns in crates/perry-runtime/src/regex.rs:

  • get_or_compile_regex() falls back to fancy_regex::Regex when regex::Regex::new() rejects the translated pattern, then stores a never-matching [^^\s\S] placeholder for the ordinary regex pointer.
  • js_string_match() checks lookup_fancy_regex(re) before using the placeholder, so lookbehind .match() can work.
  • js_regexp_exec() checks FANCY_CACHE before using the placeholder, so lookbehind .exec() can work.
  • js_regexp_test() does not check lookup_fancy_regex() or FANCY_CACHE; it only does regex.is_match(str_data), which means unsupported patterns test against the never-matching placeholder.
  • crates/perry-codegen/src/expr/instance_misc1.rs lowers regex.test(str) directly to js_regexp_test, and dynamic RegExp receiver dispatch in the runtime also calls js_regexp_test for the test method.

test-parity/known_failures.json still has test_gap_regexp_advanced for lookbehind assertions, but the current implementation suggests the remaining sharp edge is specifically the .test() path rather than all lookbehind matching.

Suggested test surface

Add focused parity coverage for:

console.log(/(?<=@)\w+/.test("user@example"));
console.log(/(?<!@)\w+/.test("user@example"));
const build = () => /(?<=@)\w+/;
console.log(build().test("user@example"));

The expected output should match Node. It would also be useful to keep .match() and .exec() assertions nearby to prevent regressions in the paths that already use the fallback.

Scope / non-goals

This issue is not asking for full RegExp spec parity, Unicode property escape support, or a replacement for the regex engine. It is scoped to routing RegExp.prototype.test() through the existing fancy_regex fallback whenever the runtime has already cached one for the pattern.

Duplicate checks

Searched issues and PRs for:

  • RegExp lookbehind
  • regex lookbehind
  • lookbehind test regex
  • RegExp.test
  • js_regexp_test lookbehind
  • fancy_regex regexp test

Related but not duplicates: closed #17 covered broad lookbehind support, and merged PR #975 wired string.match() to the fallback. I did not find an open issue for the remaining .test() path.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions