Skip to main content

atmos/os_lib/js/builtins/
regexp.rs

1// 分割: builtins.rs より機械的に移動(2026-07-16 リファクタ フェーズ2)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5// ============ RegExp ============
6
7use super::super::regex::{self, RegExpData};
8
9pub(crate) fn as_regexp(v: &Value) -> Option<PromiseRc<PromiseRefCell<RegExpData>>> {
10    if let Value::Object(o) = v {
11        let o = unwrap_proxy_target(o);
12        let b = o.borrow();
13        if let ObjKind::RegExpObj(r) = &b.kind {
14            return Some(r.clone());
15        }
16    }
17    None
18}
19
20pub(crate) fn regexp_ctor(_: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
21    let first = arg(a, 0);
22    let (pattern, flags) = if let Some(r) = as_regexp(&first) {
23        let d = r.borrow();
24        let f = if a.len() > 1 && !matches!(arg(a, 1), Value::Undefined) {
25            arg(a, 1).to_js_string()
26        } else {
27            d.re.flags.clone()
28        };
29        (d.re.source.clone(), f)
30    } else {
31        let p = if matches!(first, Value::Undefined) {
32            String::new()
33        } else {
34            first.to_js_string()
35        };
36        let f = if a.len() > 1 && !matches!(arg(a, 1), Value::Undefined) {
37            arg(a, 1).to_js_string()
38        } else {
39            String::new()
40        };
41        (p, f)
42    };
43    let re = regex::Regex::new(&pattern, &flags);
44    Ok(Value::Object(Obj::regexp(RegExpData { re, last_index: 0 })))
45}
46
47/// `RegExp.escape(str)`(ES2025)。ユーザー入力を正規表現の構文文字として解釈させない
48/// ために `new RegExp(userInput)` へ渡す前に使う定番イディオム向け。RegExp 構文上の
49/// 特殊文字(`^ $ \ . * + ? ( ) [ ] { } | /`)と一部の空白系制御文字をエスケープする
50/// 簡略実装(Unicode プロパティエスケープ等が絡む厳密なコーナーケースは対象外)。
51pub(crate) fn regexp_escape_static(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
52    let s = arg(a, 0).to_js_string();
53    let mut out = String::new();
54    for c in s.chars() {
55        match c {
56            '^' | '$' | '\\' | '.' | '*' | '+' | '?' | '(' | ')' | '[' | ']' | '{' | '}' | '|'
57            | '/' | '-' => {
58                out.push('\\');
59                out.push(c);
60            }
61            ' ' => out.push_str("\\x20"),
62            '\t' => out.push_str("\\t"),
63            '\n' => out.push_str("\\n"),
64            '\r' => out.push_str("\\r"),
65            '\u{000B}' => out.push_str("\\v"),
66            '\u{000C}' => out.push_str("\\f"),
67            _ => out.push(c),
68        }
69    }
70    Ok(Value::str(out))
71}
72
73/// `CSS.escape(str)`(CSSOM。丸ごと未対応だった)。CSS 識別子として安全な文字列へ
74/// エスケープする。WHATWG CSSOM の「serialize an identifier」アルゴリズムに準拠:
75/// NULL は U+FFFD に置換、制御文字と先頭の数字(および `-` の直後の数字)は
76/// 16進コードポイント + 空白でエスケープ、`-` 単体は `\-`、英数字/`_`/`-`/非ASCII
77/// はそのまま、それ以外の記号はバックスラッシュを前置する。
78pub(crate) fn css_escape_static(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
79    let s = arg(a, 0).to_js_string();
80    let chars: Vec<char> = s.chars().collect();
81    let mut out = String::new();
82    for (i, &c) in chars.iter().enumerate() {
83        let is_digit = c.is_ascii_digit();
84        let needs_hex_escape = c == '\u{0000}'
85            || ('\u{0001}'..='\u{001f}').contains(&c)
86            || c == '\u{007f}'
87            || (i == 0 && is_digit)
88            || (i == 1 && is_digit && chars.first() == Some(&'-'));
89        if c == '\u{0000}' {
90            out.push('\u{fffd}');
91        } else if needs_hex_escape {
92            out.push('\\');
93            out.push_str(&alloc::format!("{:x} ", c as u32));
94        } else if i == 0 && chars.len() == 1 && c == '-' {
95            out.push_str("\\-");
96        } else if c.is_ascii_alphanumeric() || c == '_' || c == '-' || (c as u32) >= 0x80 {
97            out.push(c);
98        } else {
99            out.push('\\');
100            out.push(c);
101        }
102    }
103    Ok(Value::str(out))
104}
105
106/// `CSS.supports(conditionText)` / `CSS.supports(property, value)`(CSSOM。
107/// 丸ごと未対応だった)。`@supports` の条件式評価と全く同じ
108/// `css::supports_condition_matches` をそのまま再利用する(葉の
109/// `(property: value)` 自体は「この処理系が構文解析できる宣言は常に対応して
110/// いる」扱いの簡略方針である点も含め、`@supports` と挙動を一致させる)。
111pub(crate) fn css_supports_static(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
112    let cond = if a.len() >= 2 {
113        alloc::format!("({}: {})", arg(a, 0).to_js_string(), arg(a, 1).to_js_string())
114    } else {
115        arg(a, 0).to_js_string()
116    };
117    Ok(Value::Bool(crate::os_lib::css::supports_condition_matches(&cond)))
118}
119
120/// `CSS.number()`/`.percent()`/`.px()`/`.em()`/`.rem()`/`.deg()`(CSS Typed OM。
121/// `element.attributeStyleMap`や`CSSStyleValue`の演算(`.add()`/`.mul()`等)を
122/// 伴う完全なTyped OMは丸ごと未対応(別途`Document.parseHTMLUnsafe`と同格の
123/// 「独立した実装が必要」大規模領域として`spec/TODO.md`へ記載)だが、値と
124/// 単位を保持し文字列化できるだけの`CSSUnitValue`ファクトリ関数群自体は
125/// 単純な「値+単位のオブジェクトを手動構築する」定番パターン(`DOMRect`/
126/// `DOMPoint`と同型)で自己完結して実装できるため、こちらだけ丸ごと
127/// 未対応だったものを個別に実装する。`toString()`は`"{value}{unit記号}"`
128/// を返す(`number`は単位記号無し、`percent`は`%`、それ以外は単位名を
129/// そのまま接尾辞に使う)。2026-07-18 発見・実装。
130fn make_css_unit_value(value: f64, unit: &str) -> Value {
131    let o = Obj::plain();
132    {
133        let mut b = o.borrow_mut();
134        b.props.insert("value".into(), Value::Number(value));
135        b.props.insert("unit".into(), Value::str(unit));
136        b.props
137            .insert("toString".into(), nv("toString", css_unit_value_to_string));
138    }
139    Value::Object(o)
140}
141
142fn css_unit_value_to_string(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
143    if let Value::Object(o) = &this {
144        let b = o.borrow();
145        let value = b.props.get("value").cloned().unwrap_or(Value::Number(0.0));
146        let unit = b.props.get("unit").map(|v| v.to_js_string()).unwrap_or_default();
147        let suffix = match unit.as_str() {
148            "number" => alloc::string::String::new(),
149            "percent" => alloc::string::String::from("%"),
150            other => alloc::string::String::from(other),
151        };
152        return Ok(Value::str(alloc::format!("{}{}", value.to_js_string(), suffix)));
153    }
154    Ok(Value::str(""))
155}
156
157pub(crate) fn css_number_static(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
158    Ok(make_css_unit_value(arg(a, 0).to_number(), "number"))
159}
160pub(crate) fn css_percent_static(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
161    Ok(make_css_unit_value(arg(a, 0).to_number(), "percent"))
162}
163pub(crate) fn css_px_static(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
164    Ok(make_css_unit_value(arg(a, 0).to_number(), "px"))
165}
166pub(crate) fn css_em_static(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
167    Ok(make_css_unit_value(arg(a, 0).to_number(), "em"))
168}
169pub(crate) fn css_rem_static(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
170    Ok(make_css_unit_value(arg(a, 0).to_number(), "rem"))
171}
172pub(crate) fn css_deg_static(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
173    Ok(make_css_unit_value(arg(a, 0).to_number(), "deg"))
174}
175
176/// RegExp オブジェクトのプロパティ/メソッド(interp の get_property から)。
177pub fn regexp_get(obj: &ObjRef, key: &str) -> Value {
178    let r = match &obj.borrow().kind {
179        ObjKind::RegExpObj(r) => r.clone(),
180        _ => return Value::Undefined,
181    };
182    match key {
183        "source" => Value::str(r.borrow().re.source.clone()),
184        "flags" => Value::str(r.borrow().re.flags.clone()),
185        "global" => Value::Bool(r.borrow().re.global),
186        "ignoreCase" => Value::Bool(r.borrow().re.ignorecase),
187        "multiline" => Value::Bool(r.borrow().re.multiline),
188        "dotAll" => Value::Bool(r.borrow().re.dotall),
189        "sticky" => Value::Bool(r.borrow().re.sticky),
190        "hasIndices" => Value::Bool(r.borrow().re.has_indices),
191        // `unicode`/`unicodeSets`(`u`/`v` フラグの真偽値アクセサ)が丸ごと未対応で
192        // `undefined` を返しており、`if (re.unicode)` のような定番チェックが常に偽になる
193        // バグだった。`u`/`v` フラグ自体の実際の意味論(サロゲートペア単位の走査・
194        // `\p{...}` プロパティエスケープ)は別課題として非対応のままだが、他の
195        // フラグ系ゲッター(`sticky`/`hasIndices` 等)と同じく、フラグ文字列に該当文字が
196        // 含まれるかを返すだけの単純なアクセサ自体は仕様どおり実装できる。
197        "unicode" => Value::Bool(r.borrow().re.flags.contains('u')),
198        "unicodeSets" => Value::Bool(r.borrow().re.flags.contains('v')),
199        "lastIndex" => Value::Number(r.borrow().last_index as f64),
200        "test" => nv("RegExp.test", re_test),
201        "exec" => nv("RegExp.exec", re_exec),
202        "toString" => nv("RegExp.toString", re_to_string),
203        "compile" => nv("RegExp.compile", re_compile),
204        "Symbol(Symbol.match)" => nv("[Symbol.match]", re_symbol_match),
205        "Symbol(Symbol.replace)" => nv("[Symbol.replace]", re_symbol_replace),
206        "Symbol(Symbol.search)" => nv("[Symbol.search]", re_symbol_search),
207        "Symbol(Symbol.split)" => nv("[Symbol.split]", re_symbol_split),
208        "Symbol(Symbol.matchAll)" => nv("[Symbol.matchAll]", re_symbol_match_all),
209        _ => Value::Undefined,
210    }
211}
212
213/// `RegExp.prototype.compile(pattern, flags)`(レガシーだが今も仕様に残る
214/// メソッド。丸ごと未対応だった)。`new RegExp(...)` と異なり新しいオブジェクトを
215/// 作らず、`this` 自身の内部状態(パターン・フラグ・`lastIndex`)をその場で
216/// 差し替える。`pattern` が既存の RegExp なら仕様どおりその `source`/`flags` を
217/// 継承する(`regexp_ctor` の1引数解決ロジックと同じ規則)。
218pub(crate) fn re_compile(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
219    if let Some(r) = as_regexp(&this) {
220        let first = arg(a, 0);
221        let (pattern, flags) = if let Some(src) = as_regexp(&first) {
222            let d = src.borrow();
223            let f = if a.len() > 1 && !matches!(arg(a, 1), Value::Undefined) {
224                arg(a, 1).to_js_string()
225            } else {
226                d.re.flags.clone()
227            };
228            (d.re.source.clone(), f)
229        } else {
230            let p = if matches!(first, Value::Undefined) {
231                String::new()
232            } else {
233                first.to_js_string()
234            };
235            let f = if a.len() > 1 && !matches!(arg(a, 1), Value::Undefined) {
236                arg(a, 1).to_js_string()
237            } else {
238                String::new()
239            };
240            (p, f)
241        };
242        let mut d = r.borrow_mut();
243        d.re = regex::Regex::new(&pattern, &flags);
244        d.last_index = 0;
245    }
246    Ok(this)
247}
248
249/// マッチ結果を JS 配列(index/input プロパティ付き)に変換。
250pub(crate) fn match_to_array(
251    m: &regex::Match,
252    chars: &[char],
253    input: &str,
254    group_names: &[(String, usize)],
255) -> Value {
256    match_to_array_impl(m, chars, input, false, group_names)
257}
258
259/// 名前付きキャプチャグループ(ES2018)の `groups` オブジェクトを組み立てる。
260/// `values` は各グループ番号 `idx`(0 は全体一致なので使わない)に対応する値。
261pub(crate) fn build_named_groups(group_names: &[(String, usize)], values: &[Value]) -> Value {
262    if group_names.is_empty() {
263        return Value::Undefined;
264    }
265    let g = Obj::plain();
266    for (name, idx) in group_names {
267        let v = values.get(*idx).cloned().unwrap_or(Value::Undefined);
268        g.borrow_mut().props.insert(name.clone(), v);
269    }
270    Value::Object(g)
271}
272
273pub(crate) fn match_to_array_impl(
274    m: &regex::Match,
275    chars: &[char],
276    input: &str,
277    has_indices: bool,
278    group_names: &[(String, usize)],
279) -> Value {
280    let mut items = Vec::new();
281    for cap in &m.captures {
282        match cap {
283            Some((s, e)) => items.push(Value::str(chars[*s..*e].iter().collect::<String>())),
284            None => items.push(Value::Undefined),
285        }
286    }
287    // `groups`(名前付きキャプチャグループ、ES2018)。以前は正規表現エンジン自体が
288    // `(?<name>...)` を解析せず非キャプチャにフォールバックしていたため、この
289    // プロパティは常に `undefined` だった。
290    let groups_val = build_named_groups(group_names, &items);
291    let arr = Obj::array(items);
292    arr.borrow_mut()
293        .props
294        .insert("index".into(), Value::Number(m.start as f64));
295    arr.borrow_mut()
296        .props
297        .insert("input".into(), Value::str(String::from(input)));
298    arr.borrow_mut().props.insert("groups".into(), groups_val);
299    if has_indices {
300        // `d`(hasIndices)フラグ: 各キャプチャの [start,end) 文字インデックスを持つ
301        // `indices` 配列(unmatched なグループは undefined)。
302        let indices_items: Vec<Value> = m
303            .captures
304            .iter()
305            .map(|cap| match cap {
306                Some((s, e)) => {
307                    Value::Object(Obj::array(alloc::vec![
308                        Value::Number(*s as f64),
309                        Value::Number(*e as f64)
310                    ]))
311                }
312                None => Value::Undefined,
313            })
314            .collect();
315        let indices_groups = build_named_groups(group_names, &indices_items);
316        let indices_arr = Obj::array(indices_items);
317        indices_arr.borrow_mut().props.insert("groups".into(), indices_groups);
318        arr.borrow_mut()
319            .props
320            .insert("indices".into(), Value::Object(indices_arr));
321    }
322    Value::Object(arr)
323}
324
325pub(crate) fn re_test(_: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
326    let r = match as_regexp(&this) {
327        Some(r) => r,
328        None => return Ok(Value::Bool(false)),
329    };
330    let input = arg(a, 0).to_js_string();
331    let chars: Vec<char> = input.chars().collect();
332    let sticky = r.borrow().re.sticky;
333    let uses_last_index = r.borrow().re.global || sticky;
334    let start = if uses_last_index {
335        r.borrow().last_index.min(chars.len())
336    } else {
337        0
338    };
339    let found = if sticky {
340        r.borrow().re.find_exact_at(&chars, start)
341    } else {
342        r.borrow().re.find_at(&chars, start)
343    };
344    match found {
345        Some(m) => {
346            if uses_last_index {
347                r.borrow_mut().last_index = m.end;
348            }
349            Ok(Value::Bool(true))
350        }
351        None => {
352            if uses_last_index {
353                r.borrow_mut().last_index = 0;
354            }
355            Ok(Value::Bool(false))
356        }
357    }
358}
359
360pub(crate) fn re_exec(_: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
361    let r = match as_regexp(&this) {
362        Some(r) => r,
363        None => return Ok(Value::Null),
364    };
365    let input = arg(a, 0).to_js_string();
366    let chars: Vec<char> = input.chars().collect();
367    let sticky = r.borrow().re.sticky;
368    let uses_last_index = r.borrow().re.global || sticky;
369    let start = if uses_last_index {
370        r.borrow().last_index.min(chars.len())
371    } else {
372        0
373    };
374    let found = if sticky {
375        r.borrow().re.find_exact_at(&chars, start)
376    } else {
377        r.borrow().re.find_at(&chars, start)
378    };
379    match found {
380        Some(m) => {
381            if uses_last_index {
382                r.borrow_mut().last_index = if m.end > m.start { m.end } else { m.end + 1 };
383            }
384            let has_indices = r.borrow().re.has_indices;
385            let group_names = r.borrow().re.group_names.clone();
386            Ok(match_to_array_impl(&m, &chars, &input, has_indices, &group_names))
387        }
388        None => {
389            if uses_last_index {
390                r.borrow_mut().last_index = 0;
391            }
392            Ok(Value::Null)
393        }
394    }
395}
396
397pub(crate) fn re_to_string(_: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
398    Ok(Value::str(this.to_js_string()))
399}
400
401/// `$&`/`$1`..`$99`/`$$`/`` $` ``/`$'`/`$<name>` 展開。以前は `$&`/`$1`-`$9`/`$$` のみで、
402/// `` $` ``(一致より前の部分)・`$'`(一致より後ろの部分)・`$<name>`(名前付きキャプチャ
403/// グループ、ES2018。名前付きグループ実装と同時にここも配線)・2桁のグループ番号
404/// (`$10` 以降)が非対応だった。
405pub(crate) fn expand_dollar(
406    tpl: &str,
407    m: &regex::Match,
408    chars: &[char],
409    group_names: &[(String, usize)],
410) -> String {
411    let t: Vec<char> = tpl.chars().collect();
412    let mut out = String::new();
413    let mut i = 0;
414    while i < t.len() {
415        if t[i] == '$' && i + 1 < t.len() {
416            let n = t[i + 1];
417            if n == '$' {
418                out.push('$');
419                i += 2;
420                continue;
421            }
422            if n == '&' {
423                out.push_str(&chars[m.start..m.end].iter().collect::<String>());
424                i += 2;
425                continue;
426            }
427            if n == '`' {
428                out.push_str(&chars[..m.start].iter().collect::<String>());
429                i += 2;
430                continue;
431            }
432            if n == '\'' {
433                out.push_str(&chars[m.end..].iter().collect::<String>());
434                i += 2;
435                continue;
436            }
437            if n == '<' {
438                if let Some(close) = t[i + 2..].iter().position(|&c| c == '>') {
439                    let name: String = t[i + 2..i + 2 + close].iter().collect();
440                    if let Some((_, gi)) = group_names.iter().find(|(gn, _)| *gn == name) {
441                        if let Some(Some((s, e))) = m.captures.get(*gi) {
442                            out.push_str(&chars[*s..*e].iter().collect::<String>());
443                        }
444                    }
445                    i += 2 + close + 1;
446                    continue;
447                }
448            }
449            if n.is_ascii_digit() {
450                // 2桁優先(`$10` は `$1` ではなくグループ10。存在しなければ1桁として扱う)。
451                let two_digit = if i + 2 < t.len() && t[i + 2].is_ascii_digit() {
452                    let gi2: usize = format!("{}{}", n, t[i + 2]).parse().unwrap_or(0);
453                    if gi2 > 0 && m.captures.get(gi2).is_some() {
454                        Some((gi2, 3))
455                    } else {
456                        None
457                    }
458                } else {
459                    None
460                };
461                let (gi, adv) = two_digit.unwrap_or((n.to_digit(10).unwrap_or(0) as usize, 2));
462                if let Some(Some((s, e))) = m.captures.get(gi) {
463                    out.push_str(&chars[*s..*e].iter().collect::<String>());
464                }
465                i += adv;
466                continue;
467            }
468        }
469        out.push(t[i]);
470        i += 1;
471    }
472    out
473}
474
475/// RegExp 置換(関数リプレーサ対応・g で全置換)。
476pub(crate) fn regex_replace(
477    it: &mut Interp,
478    s: &str,
479    r: &PromiseRc<PromiseRefCell<RegExpData>>,
480    repl: Value,
481    global: bool,
482) -> Result<String, Value> {
483    let chars: Vec<char> = s.chars().collect();
484    let replacer_fn = callable_or_none(repl.clone());
485    let group_names = r.borrow().re.group_names.clone();
486    let mut out = String::new();
487    let mut pos = 0usize;
488    loop {
489        let found = r.borrow().re.find_at(&chars, pos);
490        let m = match found {
491            Some(m) => m,
492            None => break,
493        };
494        out.push_str(&chars[pos..m.start].iter().collect::<String>());
495        let rep = if let Some(f) = &replacer_fn {
496            let mut args: Vec<Value> = Vec::new();
497            for cap in &m.captures {
498                match cap {
499                    Some((cs, ce)) => {
500                        args.push(Value::str(chars[*cs..*ce].iter().collect::<String>()))
501                    }
502                    None => args.push(Value::Undefined),
503                }
504            }
505            args.push(Value::Number(m.start as f64));
506            args.push(Value::str(String::from(s)));
507            // 名前付きキャプチャグループがあれば、仕様どおり最後の引数として
508            // `groups` オブジェクトを渡す(以前は名前付きグループ自体が未実装だった)。
509            if !group_names.is_empty() {
510                args.push(build_named_groups(&group_names, &args[..m.captures.len()]));
511            }
512            it.call_value(f, Value::Undefined, &args)?.to_js_string()
513        } else {
514            expand_dollar(&repl.to_js_string(), &m, &chars, &group_names)
515        };
516        out.push_str(&rep);
517        // 前進(ゼロ幅一致は 1 文字進めて取りこぼし防止)。
518        if m.end > m.start {
519            pos = m.end;
520        } else {
521            if m.start < chars.len() {
522                out.push(chars[m.start]);
523            }
524            pos = m.start + 1;
525        }
526        if !global || pos > chars.len() {
527            break;
528        }
529    }
530    out.push_str(&chars[pos.min(chars.len())..].iter().collect::<String>());
531    Ok(out)
532}
533
534pub(crate) fn str_match(it: &mut Interp, t: Value, a: &[Value]) -> Result<Value, Value> {
535    let rx = arg(a, 0);
536    if let Value::Object(_) = &rx {
537        let matcher = it.get_property(&rx, "Symbol(Symbol.match)")?;
538        if !matches!(matcher, Value::Undefined) {
539            return it.call_value(&matcher, rx.clone(), &[t]);
540        }
541    }
542    let s = this_str(&t);
543    let chars: Vec<char> = s.chars().collect();
544    let r = match as_regexp(&arg(a, 0)) {
545        Some(r) => r,
546        None => {
547            // 文字列はパターンとして RegExp 化。
548            let re = regex::Regex::new(&arg(a, 0).to_js_string(), "");
549            PromiseRc::new(PromiseRefCell::new(RegExpData { re, last_index: 0 }))
550        }
551    };
552    if r.borrow().re.global {
553        // 全一致の文字列配列。
554        let mut items = Vec::new();
555        let mut pos = 0;
556        while let Some(m) = r.borrow().re.find_at(&chars, pos) {
557            items.push(Value::str(chars[m.start..m.end].iter().collect::<String>()));
558            pos = if m.end > m.start { m.end } else { m.end + 1 };
559            if pos > chars.len() {
560                break;
561            }
562        }
563        if items.is_empty() {
564            return Ok(Value::Null);
565        }
566        return Ok(Value::Object(Obj::array(items)));
567    }
568    let found = r.borrow().re.find_at(&chars, 0);
569    match found {
570        Some(m) => {
571            let group_names = r.borrow().re.group_names.clone();
572            Ok(match_to_array(&m, &chars, &s, &group_names))
573        }
574        None => Ok(Value::Null),
575    }
576}
577
578/// `String.prototype.matchAll(regexp)`。以前は `regexp` に `g` フラグが無くても
579/// 黙って全件マッチを返しており、仕様上必須の `TypeError`(無限ループの温床になり得る
580/// ため意図的に要求される)を投げていなかった。
581pub(crate) fn str_match_all(it: &mut Interp, t: Value, a: &[Value]) -> Result<Value, Value> {
582    // 【順序が重要】仕様では `g` フラグの検査が**先**で、
583    // `[Symbol.matchAll]` への委譲はその後。
584    let rx = arg(a, 0);
585    if let Some(r) = as_regexp(&rx) {
586        if !r.borrow().re.global {
587            return Err(it.error(
588                "String.prototype.matchAll called with a non-global RegExp argument",
589            ));
590        }
591    }
592    // 独自の `[Symbol.matchAll]` を持つ相手なら、そちらへ委譲する(仕様)。
593    // `match`/`search`/`replace`/`split` は既にこの形で委譲していたが、
594    // ここだけ漏れており、自前のマッチャーが無視されていた。
595    if let Value::Object(_) = &rx {
596        let matcher = it.get_property(&rx, "Symbol(Symbol.matchAll)")?;
597        if !matches!(matcher, Value::Undefined) {
598            return it.call_value(&matcher, rx.clone(), &[t]);
599        }
600    }
601    let s = this_str(&t);
602    let chars: Vec<char> = s.chars().collect();
603    let r = match as_regexp(&arg(a, 0)) {
604        Some(r) => {
605            if !r.borrow().re.global {
606                return Err(it.error(
607                    "String.prototype.matchAll called with a non-global RegExp argument",
608                ));
609            }
610            r
611        }
612        None => {
613            let re = regex::Regex::new(&arg(a, 0).to_js_string(), "g");
614            PromiseRc::new(PromiseRefCell::new(RegExpData { re, last_index: 0 }))
615        }
616    };
617    let group_names = r.borrow().re.group_names.clone();
618    let mut items = Vec::new();
619    let mut pos = 0;
620    while let Some(m) = r.borrow().re.find_at(&chars, pos) {
621        items.push(match_to_array(&m, &chars, &s, &group_names));
622        pos = if m.end > m.start { m.end } else { m.end + 1 };
623        if pos > chars.len() {
624            break;
625        }
626    }
627    Ok(Value::Object(Obj::array(items)))
628}
629
630pub(crate) fn str_search(it: &mut Interp, t: Value, a: &[Value]) -> Result<Value, Value> {
631    let rx = arg(a, 0);
632    if let Value::Object(_) = &rx {
633        let matcher = it.get_property(&rx, "Symbol(Symbol.search)")?;
634        if !matches!(matcher, Value::Undefined) {
635            return it.call_value(&matcher, rx.clone(), &[t]);
636        }
637    }
638    let s = this_str(&t);
639    let chars: Vec<char> = s.chars().collect();
640    let r = match as_regexp(&arg(a, 0)) {
641        Some(r) => r,
642        None => {
643            let re = regex::Regex::new(&arg(a, 0).to_js_string(), "");
644            PromiseRc::new(PromiseRefCell::new(RegExpData { re, last_index: 0 }))
645        }
646    };
647    let found = r.borrow().re.find_at(&chars, 0);
648    match found {
649        Some(m) => Ok(Value::Number(m.start as f64)),
650        None => Ok(Value::Number(-1.0)),
651    }
652}
653
654/// `RegExp.prototype[Symbol.match](string)` (ES2015)
655pub(crate) fn re_symbol_match(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
656    let r = match as_regexp(&this) {
657        Some(r) => r,
658        None => return Err(it.error("RegExp.prototype[Symbol.match] called on non-RegExp")),
659    };
660    let s = arg(a, 0).to_js_string();
661    let chars: Vec<char> = s.chars().collect();
662    if r.borrow().re.global {
663        let mut items = Vec::new();
664        let mut pos = 0;
665        while let Some(m) = r.borrow().re.find_at(&chars, pos) {
666            items.push(Value::str(chars[m.start..m.end].iter().collect::<String>()));
667            pos = if m.end > m.start { m.end } else { m.end + 1 };
668            if pos > chars.len() {
669                break;
670            }
671        }
672        if items.is_empty() {
673            return Ok(Value::Null);
674        }
675        return Ok(Value::Object(Obj::array(items)));
676    }
677    let found = r.borrow().re.find_at(&chars, 0);
678    match found {
679        Some(m) => {
680            let group_names = r.borrow().re.group_names.clone();
681            Ok(match_to_array(&m, &chars, &s, &group_names))
682        }
683        None => Ok(Value::Null),
684    }
685}
686
687/// `RegExp.prototype[Symbol.replace](string, replaceValue)` (ES2015)
688pub(crate) fn re_symbol_replace(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
689    let r = match as_regexp(&this) {
690        Some(r) => r,
691        None => return Err(it.error("RegExp.prototype[Symbol.replace] called on non-RegExp")),
692    };
693    let s = arg(a, 0).to_js_string();
694    let repl = arg(a, 1);
695    let global = r.borrow().re.global;
696    Ok(Value::str(regex_replace(it, &s, &r, repl, global)?))
697}
698
699/// `RegExp.prototype[Symbol.search](string)` (ES2015)
700pub(crate) fn re_symbol_search(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
701    let r = match as_regexp(&this) {
702        Some(r) => r,
703        None => return Err(it.error("RegExp.prototype[Symbol.search] called on non-RegExp")),
704    };
705    let s = arg(a, 0).to_js_string();
706    let chars: Vec<char> = s.chars().collect();
707    let found = r.borrow().re.find_at(&chars, 0);
708    match found {
709        Some(m) => Ok(Value::Number(m.start as f64)),
710        None => Ok(Value::Number(-1.0)),
711    }
712}
713
714/// `RegExp.prototype[Symbol.split](string, limit)` (ES2015)
715pub(crate) fn re_symbol_split(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
716    let r = match as_regexp(&this) {
717        Some(r) => r,
718        None => return Err(it.error("RegExp.prototype[Symbol.split] called on non-RegExp")),
719    };
720    let s = arg(a, 0).to_js_string();
721    let limit = match arg(a, 1) {
722        Value::Undefined => usize::MAX,
723        v => {
724            let n = v.to_number();
725            if n.is_nan() || n < 0.0 {
726                usize::MAX
727            } else {
728                n as usize
729            }
730        }
731    };
732    let chars: Vec<char> = s.chars().collect();
733    let mut items = Vec::new();
734    let mut last = 0usize;
735    let mut pos = 0usize;
736    while pos <= chars.len() && items.len() < limit {
737        match r.borrow().re.find_at(&chars, pos) {
738            Some(m) if m.end > m.start => {
739                items.push(Value::str(chars[last..m.start].iter().collect::<String>()));
740                // `m.captures[0]` はこの処理系の規約で「全体一致」自体(`match_to_array_impl`
741                // と同じ規約)であり、明示キャプチャグループは添字1から始まる。
742                // 仕様上`[Symbol.split]`が結果配列に挿入すべきなのは明示キャプチャ
743                // グループのみ(全体一致テキストは分割区切りとして消費されるだけで
744                // 結果に含まれない)が、以前は`captures[0]`(全体一致)も誤って
745                // 挿入してしまい、無キャプチャの正規表現(`/b/`等)でも一致部分が
746                // 余分な要素として結果に混入するバグだった(2026-07-18 発見・修正)。
747                for cap in m.captures.iter().skip(1) {
748                    if items.len() >= limit { break; }
749                    match cap {
750                        Some((cs, ce)) => items.push(Value::str(chars[*cs..*ce].iter().collect::<String>())),
751                        None => items.push(Value::Undefined),
752                    }
753                }
754                last = m.end;
755                pos = m.end;
756            }
757            Some(m) => {
758                pos = m.start + 1;
759            }
760            None => break,
761        }
762    }
763    if items.len() < limit {
764        items.push(Value::str(chars[last..].iter().collect::<String>()));
765    }
766    items.truncate(limit);
767    Ok(Value::Object(Obj::array(items)))
768}
769
770/// `RegExp.prototype[Symbol.matchAll](string)` (ES2020)
771pub(crate) fn re_symbol_match_all(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
772    let r = match as_regexp(&this) {
773        Some(r) => r,
774        None => return Err(it.error("RegExp.prototype[Symbol.matchAll] called on non-RegExp")),
775    };
776    if !r.borrow().re.global {
777        return Err(it.error("RegExp.prototype[Symbol.matchAll] called with a non-global RegExp argument"));
778    }
779    let s = arg(a, 0).to_js_string();
780    let chars: Vec<char> = s.chars().collect();
781    let group_names = r.borrow().re.group_names.clone();
782    let mut items = Vec::new();
783    let mut pos = 0;
784    while let Some(m) = r.borrow().re.find_at(&chars, pos) {
785        items.push(match_to_array(&m, &chars, &s, &group_names));
786        pos = if m.end > m.start { m.end } else { m.end + 1 };
787        if pos > chars.len() {
788            break;
789        }
790    }
791    Ok(Value::Object(Obj::array(items)))
792}
793
794