Skip to content

runtime: complete String.prototype.matchAll iterator and validation semantics #2845

Closed

Description

@andrewtdiz

Summary

String.prototype.matchAll is implemented enough to return a materialized array of capture arrays, but it does not match Node's observable matchAll contract. Node returns an iterator, rejects non-global RegExp arguments, accepts string/non-RegExp patterns by constructing a global RegExp, starts from the RegExp's lastIndex, and each match result exposes metadata such as index, input, and groups.

This is separate from closed #317 / PR #331, which fixed the earlier hard codegen failure for basic matchAll use.

Expected Node behavior

On Node v25.9.0:

Array.from("a1b2".matchAll(/(\d)/g), m => [m[0], m[1], m.index]);
// [["1", "1", 1], ["2", "2", 3]]

"a".matchAll(/a/);
// TypeError: String.prototype.matchAll called with a non-global RegExp argument

Array.from("a.a".matchAll("."), m => [m[0], m.index]);
// [["a", 0], [".", 1], ["a", 2]]

const re = /a/g;
re.lastIndex = 2;
const seen = Array.from("a_a".matchAll(re), m => m.index);
// seen === [2]
// re.lastIndex is still 2

Current Perry evidence

Current origin/main has a runtime helper, but it is a simplified array materializer:

  • crates/perry-codegen/src/lower_string_method.rs lowers str.matchAll(arg) by unboxing arg as a regex handle and calling js_string_match_all(str, re).
  • crates/perry-runtime/src/regex.rs::js_string_match_all() returns *mut ArrayHeader, not an iterator object.
  • If the receiver or regex pointer is invalid, js_string_match_all() returns an empty array instead of applying RegExpCreate semantics for string/non-RegExp arguments or throwing for invalid non-global RegExp input.
  • The helper uses regex.captures_iter(str_data) over the whole string, with no visible check for the g flag and no use of a RegExp lastIndex starting point.
  • Inner match arrays store only full match and capture-group slots. There is no visible population of Node's index, input, or groups properties.

Suggested test surface

Add parity tests for:

  • "a".matchAll(/a/) throwing TypeError.
  • Array.from("a.a".matchAll("."), m => [m[0], m.index]) matching Node's string-pattern behavior.
  • const re = /a/g; re.lastIndex = 2; Array.from("a_a".matchAll(re), m => m.index) starting from index 2 without mutating re.lastIndex.
  • match result metadata: m.index, m.input, m.groups, and capture group values.
  • iterator shape: typeof iter.next === "function" and results are produced lazily through .next().

Scope / non-goals

This issue is only for String.prototype.matchAll argument validation, iterator shape, start-index behavior, and match-result metadata. Broader RegExp flag execution gaps such as sticky/dotAll/unicode/indices are tracked separately by #2828.

Activity

  1. andrewtdiz commented on Jun 3, 2026

    @andrewtdiz
    ContributorAuthor

    I’m taking this as a focused runtime compatibility cut: matchAll iterator shape, non-global RegExp validation, string-pattern behavior, lastIndex start handling, and match-result metadata. I’ll keep broader RegExp execution semantics out of scope.

  2. andrewtdiz commented on Jun 3, 2026

    @andrewtdiz
    ContributorAuthor

    Draft PR opened for this cut: #4223

    Scope covered:

    • String.prototype.matchAll now returns a RegExp String Iterator object.
    • Non-global RegExp arguments throw the Node-compatible TypeError.
    • String/non-RegExp, omitted, and undefined patterns route through global RegExp construction.
    • Match arrays preserve index, input, and own groups metadata, and the original regex lastIndex is honored without mutation.

    Validation is listed in the PR body.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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