Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions src/twocore/frontend/js/lower.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -4200,6 +4200,23 @@ fn lower_call_fixed(
computed: False,
..,
) -> lower_super_call(Some(method), arguments, env, ctx, ctr)
// `re[Symbol.match/replace/search/split](str, …)` — the RegExp reflection
// methods (§22.2.6.8/.11/.12/.14). A computed-key call whose key is one of
// these well-known symbols routes to the receiver-swapped String.prototype
// twin (see `lower_regex_symbol_method`).
ast.MemberExpression(
object:,
property: ast.MemberExpression(
object: ast.Identifier(name: "Symbol", ..),
property: ast.Identifier(name: sym, ..),
computed: False,
..,
),
computed: True,
..,
)
if sym == "match" || sym == "replace" || sym == "search" || sym == "split"
-> lower_regex_symbol_method(sym, object, arguments, env, ctx, ctr)
// recv.method(args) — method dispatch (array/string methods).
ast.MemberExpression(
object:,
Expand Down Expand Up @@ -5392,6 +5409,63 @@ fn object_ident_name(object: ast.Expression) -> String {
}
}

/// `re[Symbol.match/replace/search/split](string, …)` — the RegExp reflection
/// methods (§22.2.6.8 `@@match`, §22.2.6.11 `@@replace`, §22.2.6.12 `@@search`,
/// §22.2.6.14 `@@split`). Each is the receiver-swapped twin of the corresponding
/// `String.prototype` method — indeed `String.prototype.match(re)` is *defined* as
/// invoking `re[Symbol.match](s)` — so `re[Symbol.match](s)` computes exactly the
/// same result as `s.match(re)`. We therefore route to the identical runtime op
/// with the string as the receiver and the regex as the pattern argument, reusing
/// the global/sticky-aware `lastIndex` handling those ops already implement.
///
/// `sym` is the well-known symbol's name ("match" | "replace" | "search" |
/// "split"); `object` is the regex expression; `arguments` supply the string and,
/// for replace/split, the replacement value / limit. A missing string argument
/// lowers to `undefined`, which the runtime ToString-coerces to "undefined" per
/// spec. Returns the same value shape as the mirrored String method (match: exec
/// array or an array of matches or `null`; search: an index or -1; replace: the
/// spliced string; split: the pieces array).
fn lower_regex_symbol_method(
sym: String,
object: ast.Expression,
arguments: List(ast.Expression),
env: Env,
ctx: Ctx,
ctr: Int,
) -> Result(#(List(Bind), ir.Value, Int), Error) {
use #(bo, re, ctr) <- result_try(lower_expr(object, env, ctx, ctr))
use #(ba, argvals, ctr) <- result_try(lower_args(arguments, env, ctx, ctr))
let pre = list.append(bo, ba)
let str = case argvals {
[s, ..] -> s
[] -> undefined()
}
case sym {
"match" ->
Ok(bind_after(pre, ir.CallHost("js", "str_match", [str, re]), ctr))
"search" ->
Ok(bind_after(pre, ir.CallHost("js", "str_search", [str, re]), ctr))
"split" -> {
let lim = case argvals {
[_, l, ..] -> l
_ -> undefined()
}
Ok(bind_after(pre, ir.CallHost("js", "str_split", [str, re, lim]), ctr))
}
// `@@replace` maps onto String.prototype.replace with a regex search: the
// runtime replaces the first match, or every match for a global regex, and
// resets `lastIndex` to 0 when global — exactly the `@@replace` algorithm.
"replace" -> {
let repl = case argvals {
[_, r, ..] -> r
_ -> undefined()
}
Ok(bind_after(pre, ir.CallHost("js", "str_replace", [str, re, repl]), ctr))
}
_ -> Error(Unsupported("RegExp[Symbol." <> sym <> "]"))
}
}

fn lower_instance_method(
object: ast.Expression,
method: String,
Expand Down
56 changes: 56 additions & 0 deletions test/js_compiler_test.gleam
Original file line number Diff line number Diff line change
Expand Up @@ -6579,3 +6579,59 @@ pub fn ctor_in_closure_throws_test() {
)
to_float(call(m, "f", [])) |> should.equal(1.0)
}

// ==== RegExp (wave 11) ====

// §22.2.6.8 RegExp.prototype[@@match] on a NON-global regex returns the same array
// RegExpBuiltinExec produces: element 0 is the matched substring, `.index` the
// match offset, `.input` the subject string, `.length` the capture-array length.
pub fn regexp_symbol_match_nonglobal_test() {
val("/b./[Symbol.match]('abcd')[0]") |> should.equal(dyn("bc"))
num("/b./[Symbol.match]('abcd').index") |> should.equal(1.0)
val("/b./[Symbol.match]('abcd').input") |> should.equal(dyn("abcd"))
num("/b./[Symbol.match]('abcd').length") |> should.equal(1.0)
}

// §22.2.6.8 with the global flag: `lastIndex` is reset and every whole match is
// collected into a fresh array (no `index`/`input`/capture properties).
pub fn regexp_symbol_match_global_test() {
val("/\\d/g[Symbol.match]('a1b2c3').join(',')") |> should.equal(dyn("1,2,3"))
}

// No match → null (RegExpExec returns null, propagated by @@match).
pub fn regexp_symbol_match_none_test() {
num("/z/[Symbol.match]('abc') === null ? 1 : 0") |> should.equal(1.0)
}

// §22.2.6.12 RegExp.prototype[@@search] returns the code-point index of the first
// match, or -1 when there is none, ignoring the global/sticky/lastIndex state.
pub fn regexp_symbol_search_test() {
num("/b/[Symbol.search]('abc')") |> should.equal(1.0)
num("/z/[Symbol.search]('abc')") |> should.equal(-1.0)
}

// §22.2.6.11 RegExp.prototype[@@replace] replaces the first match for a plain
// regex and EVERY match for a global one; `$&` in the template expands to the
// matched substring.
pub fn regexp_symbol_replace_test() {
val("/o/[Symbol.replace]('foo', 'a')") |> should.equal(dyn("fao"))
val("/o/g[Symbol.replace]('foo', 'a')") |> should.equal(dyn("faa"))
val("/o/[Symbol.replace]('foo', '[$&]')") |> should.equal(dyn("f[o]o"))
}

// §22.2.6.11 with a function replacement: it is called with (matched, …captures,
// position, string) and its ToString result is spliced in.
pub fn regexp_symbol_replace_fn_test() {
let m =
compile(
"function f() { return /(\\w)o/[Symbol.replace]('foo', function (whole, g1) { return g1 + '!'; }); }",
)
call(m, "f", []) |> should.equal(dyn("f!o"))
}

// §22.2.6.14 RegExp.prototype[@@split] splits on the pattern; a `limit` bounds the
// number of returned pieces.
pub fn regexp_symbol_split_test() {
val("/,/[Symbol.split]('a,b,c').join('|')") |> should.equal(dyn("a|b|c"))
num("/,/[Symbol.split]('a,b,c', 2).length") |> should.equal(2.0)
}
Loading