diff --git a/src/twocore/frontend/js/lower.gleam b/src/twocore/frontend/js/lower.gleam index 066a430..2b4d83f 100644 --- a/src/twocore/frontend/js/lower.gleam +++ b/src/twocore/frontend/js/lower.gleam @@ -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:, @@ -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, diff --git a/test/js_compiler_test.gleam b/test/js_compiler_test.gleam index 3ffa639..d996b75 100644 --- a/test/js_compiler_test.gleam +++ b/test/js_compiler_test.gleam @@ -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) +}