corvus-regex-ecma262 for R

Regular expressions with the grammar and the answers of ECMA-262, the RegExp of JavaScript, for R: the corvus-regex-ecma262 Rust crate as the R package corvusregexecma262, for R 4.2 and later.

Compliance. Over the ECMA-262 test data (what V8 says about 123,087 patterns and 6,056,699 texts, test262's and exec, the global search, replace and split among them, and texts of bytes that are not UTF-8, which it refuses), through this package over R's strings, raw vectors and ecma_utf16 code units, it gives 29,289,554 right answers, every answer the data asks, and none wrong, refuses no valid pattern, accepts no invalid one and reaches no limit. CI runs the whole data. (A text with a NUL, which an R string cannot hold, is asked as a raw vector, and the answers over UTF-16 as code units.)

Install

From R-universe:

install.packages("corvusregexecma262", repos = c("https://corvus-dotnet.r-universe.dev", "https://cloud.r-project.org"))

R-universe has binary packages for Windows and macOS. A source install builds the native code, and needs Rust 1.89 or later.

Getting started

library(corvusregexecma262)

# The flags are those of new RegExp(pattern, flags).
date <- ecma_regex("(?<year>\\d{4})-(?<month>\\d{2})", "g")

ecma_test(c("due 2026-10", "none"), date)                    # TRUE FALSE
ecma_match("due 2026-10", date)[, "year"]                    # "2026"
ecma_locate("due 2026-10", date)                             # start 5, end 11

# Every match of a global search, as matchAll makes it.
ecma_match_all("2026-10, 2027-01", date)[[1]][, 1]           # "2026-10" "2027-01"

# replace replaces every match with the g flag, and the first without it; a function may give each replacement.
ecma_replace("2026-10, 2027-01", date, "$<month>/$1")        # "10/2026, 01/2027"
ecma_replace("2026-10", date, function(m) m[["month"]])      # "10"

# split splices in what the groups captured; a group that did not take part is NA.
ecma_split("a ; b;c", "\\s*(;)\\s*")                         # list(c("a", ";", "b", ";", "c"))

The API

ecma_regex(pattern, flags = "", timeout = NULL) Compiles a pattern as new RegExp(pattern, flags) does. timeout is the match timeout in seconds (0 for none; NULL for the default).
ecma_set_default_timeout(seconds), ecma_default_timeout() The timeout of every pattern compiled without one: NULL (no limit) unless set.
ecma_valid(pattern, flags = "") Whether each pattern is valid, without compiling it.
ecma_test(x, regex, start = 1L, timeout = NULL) RegExp.prototype.test for each string, with lastIndex at start (at start alone with y).
ecma_match(x, regex, start = 1L, timeout = NULL) The first match in each string and what each group captured (exec): a character matrix.
ecma_locate(x, regex, group = 0L, start = 1L, timeout = NULL) Where the first match, or one of its groups, starts and ends: an integer matrix.
ecma_match_all(x, regex) Every match of a global search in each string (matchAll): a list of matrices.
ecma_locate_all(x, regex, group = 0L) Where each match, or one of its groups, starts and ends: a list of matrices.
ecma_count(x, regex) How many matches a global search finds, without making them.
ecma_replace(x, regex, replacement) String.prototype.replace: the first match, or every match with g, by a template or a function.
ecma_replace_all(x, regex, replacement) String.prototype.replaceAll: every match.
ecma_split(x, regex, limit = NULL) String.prototype.split: a list of character vectors.
ecma_utf16(x) The UTF-16 code units of a string or of a raw vector of UTF-8, or code units given as numbers: a text with JavaScript's code units, which every function takes. as.character and as.raw turn one back.
ecma_version() The package's and the crate's versions.

A match's row is the match, then each group, then one column for each group name (of the groups that share a name, the one that took part); NA is a group that did not take part. There is NA (or NULL in a list) for an NA string.

Timeouts. A pattern has a match timeout, as a Regex of .NET has: ecma_regex(pattern, flags, timeout = 1), or for every pattern compiled after it ecma_set_default_timeout(1). Every function that matches gives each of its matches the timeout and raises ecma_match_limit at it; the single searches take a timeout of their own in its place. Without a timeout (the default) a match has no limit of time or of memory, so a program that matches a pattern or a text it did not write should set one.

Positions. Positions are in characters from 1, as nchar counts them, an end inclusive, as regexpr gives them; an empty match ends one character before it starts. Without u or v a match can take half of a character beyond the Basic Multilingual Plane: its text then holds that half as U+FFFD, and its positions are the whole character's.

Texts that are not strings. x may also be a raw vector (one text of UTF-8 bytes, which may hold a NUL), an ecma_utf16 (one text of UTF-16 code units, which may hold lone surrogates), or a list of them, NULL for a missing text. Positions are then in bytes or in code units, and the texts of a match, a replacement and a split's pieces come back in the form of the text (a match's row is a list matrix). A raw vector that is not UTF-8 is refused with ecma_encoding_error, never read in part. A pattern may be a raw vector too, so that it can hold a NUL.

ecma_locate(ecma_utf16(c(0x61, 0xD800, 0x62)), "\\uD800")        # start 2, end 2
ecma_replace(as.raw(c(0x61, 0x00, 0x62)), "\\0", "-")             # as.raw(c(0x61, 0x2d, 0x62))

Strings. A string marked as UTF-8, or in the native encoding of a UTF-8 session, is read in place (after a check that it is UTF-8, which a long string has once while it is matched again and again); one in another encoding is translated. A string marked as bytes is refused.

Conditions. ecma_invalid_pattern, ecma_invalid_flags, ecma_pattern_too_large, ecma_match_limit (a match that reached its timeout or its cap on backtracking memory), ecma_encoding_error and ecma_argument_error, each an ecma_regex_error.

Benchmarks

The Industry workloads of dotnet/performance, this package against base R's gregexpr (PCRE2 and TRE), stringi (ICU) and V8, are on the central page, corvus-oss.org/corvus-regex-ecma262-benchmarks, measured nightly on one runner with every other Corvus ECMA-262 engine.

Repository

Releasing

A release is a GitHub release tagged v<version>, the Version in DESCRIPTION (and the version in src/rust/Cargo.toml). R-universe builds the latest release (its registry entry names the branch *release), and the release publishes the harness image with the release's tag.

Licence

Apache-2.0. See LICENSE.