Skip to main content

atmos/os_lib/js/builtins/
static_classlist.rs

1// 分割: builtins.rs より機械的に移動(2026-07-16 リファクタ フェーズ2)。
2// ロジック不変。可視性のみ pub(crate) へ昇格し、親が pub(crate) use で再エクスポート。
3use super::*;
4
5// ===== String / Array 静的 =====
6/// `String.fromCharCode(...codes)`(ES1)が `String.fromCodePoint`(ES2015)と全く同じ
7/// 実装を共有しており、2つのバグがあった。(1) 各引数を 16bit コード単位へマスクしておらず、
8/// 仕様上 `fromCharCode(0x1F600)` は単一の UTF-16 コード単位(`0xF600` 相当)になるべき
9/// ところ絵文字そのものになっていた。(2) サロゲートペア(`fromCharCode(0xD83D,0xDE00)` で
10/// 絵文字1文字を表す ES1 以来の定番イディオム)を一切結合しておらず、個々の値は単独では
11/// 無効な Unicode スカラー値のため `char::from_u32` が `None` を返し無音で消えていた。
12/// UTF-16 コード単位列として正しくデコードする(`String::from_utf16_lossy` がサロゲート
13/// ペアの結合を担う)ことで両方解消する。
14pub(crate) fn string_from_char_code(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
15    let units: Vec<u16> = a
16        .iter()
17        .map(|v| (v.to_number() as i64 as u32 & 0xFFFF) as u16)
18        .collect();
19    Ok(Value::str(String::from_utf16_lossy(&units)))
20}
21/// `String.fromCodePoint(...codePoints)`(ES2015)。`fromCharCode` と異なり各引数は
22/// フルの Unicode コードポイント(〜0x10FFFF)で、16bit マスクやサロゲートペア結合は
23/// 行わない。不正なコードポイントは仕様どおり `RangeError` 相当の例外にする。
24pub(crate) fn string_from_code_point(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
25    let mut s = String::new();
26    for v in a {
27        let n = v.to_number();
28        if !n.is_finite() || n < 0.0 || n > 0x10FFFF as f64 || libm::floor(n) != n {
29            return Err(it.error("Invalid code point"));
30        }
31        match char::from_u32(n as u32) {
32            Some(c) => s.push(c),
33            None => return Err(it.error("Invalid code point")),
34        }
35    }
36    Ok(Value::str(s))
37}
38pub(crate) fn array_of(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
39    Ok(Value::Object(Obj::array(a.to_vec())))
40}
41/// `String.raw({raw: [...]}, ...substitutions)`(ES2015)。タグ付きテンプレートの
42/// `` String.raw`a\nb${x}c` `` で使う定番の組込みタグ関数。`raw` 配列の要素を
43/// エスケープ解決せずそのまま連結し、間に `substitutions` を挟む。
44pub(crate) fn string_raw(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
45    let strings = arg(a, 0);
46    let raw = it.get_property(&strings, "raw").unwrap_or(Value::Undefined);
47    let raw_items: Vec<Value> = match &raw {
48        Value::Object(o) => match &o.borrow().kind {
49            ObjKind::Array(items) => items.clone(),
50            _ => Vec::new(),
51        },
52        _ => Vec::new(),
53    };
54    let mut out = String::new();
55    for (i, r) in raw_items.iter().enumerate() {
56        out.push_str(&r.to_js_string());
57        if let Some(sub) = a.get(i + 1) {
58            out.push_str(&sub.to_js_string());
59        }
60    }
61    Ok(Value::str(out))
62}
63
64// ===== classList 追加(item / value / toString)=====
65pub fn dom_classlist_item(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
66    if let Some(idx) = this_host_idx(&this, "classList:") {
67        let i = arg(a, 0).to_number() as usize;
68        let cls = it
69            .dom
70            .borrow()
71            .nodes
72            .get(idx)
73            .and_then(|n| n.classes.get(i).cloned());
74        return Ok(match cls {
75            Some(c) => Value::str(c),
76            None => Value::Null,
77        });
78    }
79    Ok(Value::Null)
80}
81pub fn dom_classlist_to_string(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
82    if let Some(idx) = this_host_idx(&this, "classList:") {
83        return Ok(Value::str(
84            it.dom
85                .borrow()
86                .nodes
87                .get(idx)
88                .map(|n| n.classes.join(" "))
89                .unwrap_or_default(),
90        ));
91    }
92    Ok(Value::str(""))
93}
94
95/// グローバルの location オブジェクトの各成分を url から更新する(メソッドは保持)。
96pub fn update_location(global: &Rc<RefCell<Scope>>, url: &str) {
97    let loc = match global.borrow().vars.get("location") {
98        Some(Value::Object(o)) => o.clone(),
99        _ => return,
100    };
101    let mut b = loc.borrow_mut();
102    for (k, v) in location_components(url) {
103        b.props.insert(String::from(k), Value::str(v));
104    }
105}
106
107/// 相対URLを base(現在ページの絶対URL)基準で解決する。
108/// 絶対(http/https)・`data:` はそのまま。`/...` は scheme+host 直下、その他はディレクトリ相対。
109/// `string_slice` lint 回避のため split_once / str::get のみ使用。
110/// パスセグメントの `.`/`..` を解決する(`RFC 3986 §5.2.4` の簡略版)。
111/// 以前は `resolve_url` がディレクトリ相対結合のみで `.`/`..` をそのまま
112/// 文字列結合していたため、`new URL('../style.css', 'https://h.com/a/b/c')`
113/// のような(相対URLで最も頻繁に使われる)親ディレクトリ参照が
114/// `https://h.com/a/b/../style.css` のように未解決のまま残るバグだった。
115pub(crate) fn normalize_dot_segments(path: &str) -> String {
116    let mut segs: alloc::vec::Vec<&str> = alloc::vec::Vec::new();
117    for seg in path.split('/') {
118        match seg {
119            "" | "." => {}
120            ".." => {
121                segs.pop();
122            }
123            s => segs.push(s),
124        }
125    }
126    alloc::format!("/{}", segs.join("/"))
127}
128
129pub fn resolve_url(base: &str, url: &str) -> String {
130    if url.starts_with("http://") || url.starts_with("https://") || url.starts_with("data:") {
131        return String::from(url);
132    }
133    // プロトコル相対URL(`//host/path`。`<script src="//cdn.example.com/x.js">`
134    // 等の定番パターン)が丸ごと未対応で、ベースのホスト配下の相対パスとして
135    // 誤解決されるバグだった(`//cdn.example.com/x.js` が本来
136    // `https://cdn.example.com/x.js`(別ホスト)になるべきところ、
137    // `https://e.com/cdn.example.com/x.js`(元のホスト配下のパス)という
138    // 壊れた結果になっていた)。ベースのスキームを引き継ぎ、残りをそのまま
139    // ホスト+パスとして使う。
140    if let Some(rest) = url.strip_prefix("//") {
141        let scheme = if base.starts_with("https://") { "https://" } else { "http://" };
142        return alloc::format!("{scheme}{rest}");
143    }
144    let (scheme, after) = if let Some(a) = base.strip_prefix("https://") {
145        ("https://", a)
146    } else if let Some(a) = base.strip_prefix("http://") {
147        ("http://", a)
148    } else {
149        return String::from(url); // base が使えない → そのまま(後段でエラー判定)
150    };
151    let (host, path) = match after.split_once('/') {
152        Some((h, p)) => (h, p), // p は先頭スラッシュ無しのパス
153        None => (after, ""),
154    };
155    // クエリのみ(`?x=2`)/フラグメントのみ(`#frag`)の相対参照は、仕様上
156    // ベースのパス(`?` のみの場合はクエリより前の部分)を保持したまま該当
157    // 部分だけ置き換える必要があるが、以前は他の相対パスと同じ「ディレクトリ
158    // 相対」ロジックに落ちてベースのパスの最後のセグメント(ファイル名)ごと
159    // 消えてしまうバグだった(`new URL('?x=2', 'https://e.com/a/b?x=1').href`
160    // が `https://e.com/a/?x=2` という `b` の消えた壊れた結果になっていた)。
161    if let Some(rest) = url.strip_prefix('?') {
162        let base_path = path.split('?').next().unwrap_or("").split('#').next().unwrap_or("");
163        return alloc::format!("{scheme}{host}/{base_path}?{rest}");
164    }
165    if let Some(rest) = url.strip_prefix('#') {
166        let base_path_and_query = path.split('#').next().unwrap_or("");
167        return alloc::format!("{scheme}{host}/{base_path_and_query}#{rest}");
168    }
169    let raw_path = if let Some(rooted) = url.strip_prefix('/') {
170        alloc::format!("/{rooted}")
171    } else {
172        // ディレクトリ相対: path の最後のセグメント(ファイル名)を捨てる。
173        let dir = match path.rfind('/') {
174            Some(i) => path.get(..i).unwrap_or(""),
175            None => "",
176        };
177        if dir.is_empty() {
178            alloc::format!("/{url}")
179        } else {
180            alloc::format!("/{dir}/{url}")
181        }
182    };
183    alloc::format!("{scheme}{host}{}", normalize_dot_segments(&raw_path))
184}
185
186/// 同期 HTTP リクエストの共通処理。(status, body) を返す。`data:` と `http(s):` 対応(fetch/XHR 共有)。
187/// `base` が非空なら相対URLを解決。method は GET/POST/PUT/DELETE/PATCH/HEAD 等を任意に通す
188/// (web_request 内で英大文字トークン検証)。
189pub(crate) fn do_http_request(
190    base: &str,
191    url: &str,
192    method: &str,
193    body: &str,
194    content_type: &str,
195) -> Result<(u16, String), &'static str> {
196    let resolved = resolve_url(base, url);
197    if resolved.starts_with("data:") {
198        return match data_url_body(&resolved) {
199            Some(b) => Ok((200, b)),
200            None => Err("malformed data URL"),
201        };
202    }
203    let (is_https, host, path) = match split_http_url(&resolved) {
204        Some(t) => t,
205        None => return Err("only absolute http(s) and data URLs are supported"),
206    };
207    let stack = crate::kernel::net_stack::TcpIpStack::new();
208    match stack.web_request(
209        is_https,
210        &host,
211        &path,
212        method,
213        content_type,
214        body.as_bytes(),
215    ) {
216        Ok(resp) => Ok((resp.status_code, resp.body)),
217        Err(_) => Err("network error"),
218    }
219}
220
221/// Value::Object のプロパティを取得(非オブジェクトは None)。
222pub(crate) fn obj_prop(v: &Value, key: &str) -> Option<Value> {
223    if let Value::Object(o) = v {
224        o.borrow().props.get(key).cloned()
225    } else {
226        None
227    }
228}
229
230/// `Headers`(Fetch API。`new Headers(init)`/`response.headers.get(...)` という
231/// 定番パターンで使われる get/set/has/append/delete 付きの専用クラス)が丸ごと
232/// 未対応で、`headers` は単なるプレーンオブジェクトとしてしか扱えなかった。
233/// ヘッダ名は仕様どおり大小無視・小文字正規化して格納する(プロパティキー自体を
234/// 小文字化したヘッダ名として使う。メソッド名 "get"/"set" 等と衝突するヘッダ名
235/// は理論上あり得るが実用上まず出現しないため簡略化として許容する)。
236pub(crate) fn headers_ctor(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
237    Ok(make_headers(arg(a, 0)))
238}
239/// `headers_ctor` の実体(`Interp` を必要としないため `response.headers` の
240/// 生成箇所からも直接呼べる)。
241pub(crate) fn make_headers(init: Value) -> Value {
242    let o = Obj::plain();
243    // init: 別の Headers(`_is_headers` マーカーで判別)/ プレーンオブジェクト /
244    // `[[k,v], ...]` 形式の配列、のいずれも受け付ける。
245    if let Value::Object(io) = &init {
246        let is_headers_like = io.borrow().props.contains_key("_is_headers");
247        let is_array = matches!(&io.borrow().kind, ObjKind::Array(_));
248        if is_array {
249            if let ObjKind::Array(items) = &io.borrow().kind {
250                for pair in items {
251                    if let Value::Object(p) = pair {
252                        if let ObjKind::Array(kv) = &p.borrow().kind {
253                            if kv.len() >= 2 {
254                                let k = kv[0].to_js_string().to_lowercase();
255                                o.borrow_mut().props.insert(k, Value::str(kv[1].to_js_string()));
256                            }
257                        }
258                    }
259                }
260            }
261        } else {
262            let entries: Vec<(String, Value)> =
263                io.borrow().props.iter().map(|(k, v)| (k.clone(), v.clone())).collect();
264            for (k, v) in entries {
265                // Headers 自身のメソッドプロパティ(callable)は値としてコピーしない。
266                if matches!(&v, Value::Object(f) if f.borrow().is_callable()) {
267                    continue;
268                }
269                let key = k.to_lowercase();
270                o.borrow_mut().props.insert(key, Value::str(v.to_js_string()));
271            }
272            let _ = is_headers_like;
273        }
274    }
275    {
276        let mut b = o.borrow_mut();
277        b.props.insert("_is_headers".into(), Value::Bool(true));
278        b.props.insert("get".into(), nv("get", headers_get));
279        b.props.insert("set".into(), nv("set", headers_set));
280        b.props.insert("append".into(), nv("append", headers_append));
281        b.props.insert("has".into(), nv("has", headers_has));
282        b.props.insert("delete".into(), nv("delete", headers_delete));
283        b.props
284            .insert("forEach".into(), nv("forEach", headers_for_each));
285        // `entries`/`keys`/`values`/`Symbol.iterator`(`for (const [k,v] of headers)`
286        // という定番イディオムを含む)が丸ごと未対応だった。
287        b.props.insert("entries".into(), nv("entries", headers_entries));
288        b.props.insert("keys".into(), nv("keys", headers_keys));
289        b.props.insert("values".into(), nv("values", headers_values));
290        b.props.insert(
291            "getSetCookie".into(),
292            nv("getSetCookie", headers_get_set_cookie),
293        );
294        b.props.insert(
295            "Symbol(Symbol.iterator)".into(),
296            nv("[Symbol.iterator]", headers_entries),
297        );
298    }
299    Value::Object(o)
300}
301pub(crate) fn headers_key(a: &[Value]) -> String {
302    arg(a, 0).to_js_string().to_lowercase()
303}
304pub(crate) fn headers_get_set_cookie(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
305    if let Value::Object(o) = &this {
306        if let Some(v) = o.borrow().props.get("set-cookie") {
307            if !matches!(v, Value::Object(f) if f.borrow().is_callable()) {
308                let val_str = v.to_js_string();
309                if !val_str.is_empty() {
310                    let cookies: Vec<Value> = val_str
311                        .split(", ")
312                        .map(|s| Value::str(s.to_string()))
313                        .collect();
314                    return Ok(Value::Object(Obj::array(cookies)));
315                }
316            }
317        }
318    }
319    Ok(Value::Object(Obj::array(Vec::new())))
320}
321/// `Headers` の実データ(メソッド/内部マーカーを除いた header 名/値)を、仕様どおり
322/// 名前順にソートして返す(`forEach`/`entries`/`keys`/`values`/`Symbol.iterator`
323/// が共有する)。
324pub(crate) fn headers_entries_sorted(this: &Value) -> Vec<(String, String)> {
325    let mut entries: Vec<(String, String)> = match this {
326        Value::Object(o) => o
327            .borrow()
328            .props
329            .iter()
330            .filter(|(k, v)| {
331                k.as_str() != "_is_headers" && !matches!(v, Value::Object(f) if f.borrow().is_callable())
332            })
333            .map(|(k, v)| (k.clone(), v.to_js_string()))
334            .collect(),
335        _ => Vec::new(),
336    };
337    entries.sort_by(|a, b| a.0.cmp(&b.0));
338    entries
339}
340pub(crate) fn headers_get(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
341    if let Value::Object(o) = &this {
342        if let Some(v) = o.borrow().props.get(&headers_key(a)) {
343            if !matches!(v, Value::Object(f) if f.borrow().is_callable()) {
344                return Ok(v.clone());
345            }
346        }
347    }
348    Ok(Value::Null)
349}
350pub(crate) fn headers_set(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
351    if let Value::Object(o) = &this {
352        o.borrow_mut()
353            .props
354            .insert(headers_key(a), Value::str(arg(a, 1).to_js_string()));
355    }
356    Ok(Value::Undefined)
357}
358/// `append(name, value)`。既存値があれば `", "` 区切りで連結する(仕様どおり
359/// 複数値ヘッダのコンマ結合表現。`set-cookie` 等の例外は非対応)。
360pub(crate) fn headers_append(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
361    if let Value::Object(o) = &this {
362        let key = headers_key(a);
363        let v = arg(a, 1).to_js_string();
364        let existing = o.borrow().props.get(&key).map(|v| v.to_js_string());
365        let merged = match existing {
366            Some(e) if !e.is_empty() => alloc::format!("{}, {}", e, v),
367            _ => v,
368        };
369        o.borrow_mut().props.insert(key, Value::str(merged));
370    }
371    Ok(Value::Undefined)
372}
373pub(crate) fn headers_has(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
374    if let Value::Object(o) = &this {
375        let key = headers_key(a);
376        let has = matches!(o.borrow().props.get(&key), Some(v) if !matches!(v, Value::Object(f) if f.borrow().is_callable()));
377        return Ok(Value::Bool(has));
378    }
379    Ok(Value::Bool(false))
380}
381pub(crate) fn headers_delete(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
382    if let Value::Object(o) = &this {
383        o.borrow_mut().props.shift_remove(&headers_key(a));
384    }
385    Ok(Value::Undefined)
386}
387pub(crate) fn headers_for_each(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
388    let cb = arg(a, 0);
389    for (k, v) in headers_entries_sorted(&this) {
390        it.call_value(&cb, Value::Undefined, &[Value::str(v), Value::str(k), this.clone()])?;
391    }
392    Ok(Value::Undefined)
393}
394/// `headers.entries()`/`for (const [k,v] of headers)`(`Symbol.iterator` は
395/// `entries()` と同義)が丸ごと未対応だった。
396pub(crate) fn headers_entries(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
397    let items: Vec<Value> = headers_entries_sorted(&this)
398        .into_iter()
399        .map(|(k, v)| Value::Object(Obj::array(alloc::vec![Value::str(k), Value::str(v)])))
400        .collect();
401    Ok(Value::Object(make_iterator(items)))
402}
403pub(crate) fn headers_keys(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
404    let items: Vec<Value> = headers_entries_sorted(&this)
405        .into_iter()
406        .map(|(k, _)| Value::str(k))
407        .collect();
408    Ok(Value::Object(make_iterator(items)))
409}
410pub(crate) fn headers_values(_it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
411    let items: Vec<Value> = headers_entries_sorted(&this)
412        .into_iter()
413        .map(|(_, v)| Value::str(v))
414        .collect();
415    Ok(Value::Object(make_iterator(items)))
416}
417
418/// headers オブジェクトから Content-Type を大小無視で探す。
419pub(crate) fn headers_content_type(headers: &Value) -> Option<String> {
420    if let Value::Object(o) = headers {
421        for (k, val) in o.borrow().props.iter() {
422            if k.eq_ignore_ascii_case("content-type") {
423                return Some(val.to_js_string());
424            }
425        }
426    }
427    None
428}
429
430/// fetch/XHR の body 値を文字列化する。FormData / URLSearchParams は内部 `_query`
431/// ("k=v&k=v")をそのまま送信本文に使う。それ以外は通常の文字列化。
432pub(crate) fn body_to_string(v: &Value) -> String {
433    if let Value::Object(o) = v {
434        if let Some(q) = o.borrow().props.get("_query") {
435            return q.to_js_string();
436        }
437    }
438    v.to_js_string()
439}
440
441/// `new Request(input, options?)`(Fetch API。`fetch(new Request(url, opts))` という
442/// 定番パターンで使われる)が丸ごと未対応で `Request is not defined` になっていた。
443/// `input` は URL 文字列、または別の `Request`(`.url`/`.method` 等をコピーする
444/// clone 的な使い方)のどちらも受け付ける。
445pub(crate) fn request_ctor(_it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
446    let input = arg(a, 0);
447    let opts = arg(a, 1);
448    let is_request_like = matches!(&input, Value::Object(o) if o.borrow().props.contains_key("url"));
449    let url = if is_request_like {
450        obj_prop(&input, "url").map(|v| v.to_js_string()).unwrap_or_default()
451    } else {
452        input.to_js_string()
453    };
454    let method = obj_prop(&opts, "method")
455        .or_else(|| if is_request_like { obj_prop(&input, "method") } else { None })
456        .map(|v| v.to_js_string())
457        .unwrap_or_else(|| String::from("GET"));
458    let body = obj_prop(&opts, "body")
459        .or_else(|| if is_request_like { obj_prop(&input, "_body") } else { None })
460        .map(|v| v.to_js_string())
461        .unwrap_or_default();
462    // `request.headers` が仕様上は常に本物の `Headers` インスタンス(未指定でも
463    // 空の `Headers`)であるべきところ、渡された生の値(プレーンオブジェクト/
464    // `undefined`)をそのまま格納するだけで、`request.headers.get(...)` が
465    // 「関数ではない」になるバグだった。
466    let headers_raw = obj_prop(&opts, "headers")
467        .or_else(|| if is_request_like { obj_prop(&input, "headers") } else { None })
468        .unwrap_or(Value::Undefined);
469    let headers = make_headers(headers_raw);
470    let signal = obj_prop(&opts, "signal")
471        .or_else(|| if is_request_like { obj_prop(&input, "signal") } else { None })
472        .unwrap_or(Value::Undefined);
473    let req = Obj::plain();
474    {
475        let mut r = req.borrow_mut();
476        r.props.insert("url".into(), Value::str(url));
477        r.props.insert("method".into(), Value::str(method));
478        r.props.insert("_body".into(), Value::str(body));
479        r.props.insert("headers".into(), headers);
480        r.props.insert("signal".into(), signal);
481    }
482    Ok(Value::Object(req))
483}
484
485/// `navigator.sendBeacon(url, data)`(丸ごと未対応だった。ページ離脱時の
486/// 計測データ送信に使われる定番パターン)。この処理系の HTTP は同期実行の
487/// ため、仕様上の「ページ破棄後も配送を継続する」非同期性の恩恵は無いが、
488/// 既存の `fetch()` と同じ POST 経路(同じ body/content-type 推論)を通して
489/// その場で送信し、常に成功(`true`)を返す簡略実装。戻り値の Promise は
490/// 仕様どおり呼び出し元へ返さないため破棄する。
491pub(crate) fn navigator_send_beacon(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
492    let url = arg(a, 0);
493    let data = arg(a, 1);
494    let opts = Obj::plain();
495    opts.borrow_mut().props.insert("method".into(), Value::str("POST"));
496    if !matches!(data, Value::Undefined) {
497        opts.borrow_mut().props.insert("body".into(), data);
498    }
499    let _ = fetch(it, Value::Undefined, &[url, Value::Object(opts)]);
500    Ok(Value::Bool(true))
501}
502
503/// `GeolocationPositionError` 相当のオブジェクトを組み立てる(`code`/`message`/
504/// `PERMISSION_DENIED`/`POSITION_UNAVAILABLE`/`TIMEOUT` 定数。仕様どおり
505/// インスタンスにも static 同名定数が生えている)。
506pub(crate) fn make_geolocation_error() -> Value {
507    let o = Obj::plain();
508    let mut b = o.borrow_mut();
509    b.props.insert("code".into(), Value::Number(1.0));
510    b.props.insert(
511        "message".into(),
512        Value::str("Geolocation is not supported in this environment"),
513    );
514    b.props.insert("PERMISSION_DENIED".into(), Value::Number(1.0));
515    b.props.insert("POSITION_UNAVAILABLE".into(), Value::Number(2.0));
516    b.props.insert("TIMEOUT".into(), Value::Number(3.0));
517    drop(b);
518    Value::Object(o)
519}
520/// `navigator.geolocation.getCurrentPosition(success, error, options)`(丸ごと
521/// 未対応だった)。この処理系には位置情報を提供する手段が一切無いため、常に
522/// `error` を `PERMISSION_DENIED` で同期的に呼ぶ簡略実装(`success` は永遠に
523/// 呼ばれない)。
524pub(crate) fn geolocation_get_current_position(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
525    if let Some(err_cb) = callable_or_none(arg(a, 1)) {
526        it.call_listener(
527            &err_cb,
528            Value::Undefined,
529            &[make_geolocation_error()],
530            "geolocation error callback",
531        );
532    }
533    Ok(Value::Undefined)
534}
535/// `navigator.geolocation.watchPosition(success, error, options)`。継続監視の
536/// 概念が無いためワンショットの `getCurrentPosition` と同じ即時エラー通知の
537/// みだが、`clearWatch` と対にするための一意な id は仕様どおり返す。
538pub(crate) fn geolocation_watch_position(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
539    geolocation_get_current_position(it, this, a)?;
540    Ok(Value::Number(TIMER_ID.fetch_add(1, Ordering::Relaxed) as f64))
541}
542/// `navigator.geolocation.clearWatch(id)`。監視自体が実在しないため no-op。
543pub(crate) fn geolocation_clear_watch(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
544    Ok(Value::Undefined)
545}
546/// `navigator.permissions.query({name})`(丸ごと未対応だった)。`PermissionStatus`
547/// 相当の `{state, name, onchange}` を解決済み Promise で返す。実プロンプト UI が
548/// 無いため、他 API の実装状況と整合する固定値のみを返す簡略実装。
549pub(crate) fn permissions_query(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
550    let name = obj_prop(&arg(a, 0), "name").map(|v| v.to_js_string()).unwrap_or_default();
551    let state = match name.as_str() {
552        "geolocation" | "camera" | "microphone" => "denied",
553        "notifications" => "granted",
554        _ => "prompt",
555    };
556    let status = Obj::plain();
557    {
558        let mut b = status.borrow_mut();
559        b.props.insert("state".into(), Value::str(state));
560        b.props.insert("name".into(), Value::str(name));
561        b.props.insert("onchange".into(), Value::Null);
562    }
563    Ok(resolved_promise(it, Value::Object(status)))
564}
565
566/// `navigator.wakeLock.request(type)`(Screen Wake Lock API。丸ごと未対応
567/// だった)。実際の電源管理機構は無いため、`WakeLockSentinel` 相当の
568/// `{released, type, release()}` を状態のみ追跡する簡略実装で返す。
569pub(crate) fn wake_lock_request(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
570    let ty = arg(a, 0).to_js_string();
571    let ty = if ty.is_empty() { String::from("screen") } else { ty };
572    let sentinel = Obj::plain();
573    {
574        let mut b = sentinel.borrow_mut();
575        b.props.insert("released".into(), Value::Bool(false));
576        b.props.insert("type".into(), Value::str(ty));
577        b.props
578            .insert("release".into(), nv("WakeLockSentinel.release", wake_lock_sentinel_release));
579    }
580    Ok(resolved_promise(it, Value::Object(sentinel)))
581}
582/// `WakeLockSentinel.prototype.release()`(対をなす解放メソッド)。
583pub(crate) fn wake_lock_sentinel_release(it: &mut Interp, this: Value, _a: &[Value]) -> Result<Value, Value> {
584    if let Value::Object(o) = &this {
585        o.borrow_mut().props.insert("released".into(), Value::Bool(true));
586    }
587    Ok(resolved_promise(it, Value::Undefined))
588}
589
590/// `navigator.canShare(data)`(Web Share API。丸ごと未対応だった)。実際の
591/// 共有先 UI が無くいかなる `data` も共有できないため、仕様上の「対応不明」
592/// を意味する誠実な `false` を常に返す簡略実装。
593pub(crate) fn navigator_can_share(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
594    Ok(Value::Bool(false))
595}
596/// `navigator.share(data)`(Web Share API。丸ごと未対応だった)。実際の
597/// 共有先 UI が無いため、ユーザーがネイティブ共有シートを閉じた場合と同じ
598/// 仕様どおりの `AbortError` で常に拒否する(成功を偽装しない)。
599pub(crate) fn navigator_share(it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
600    let err = Obj::plain();
601    {
602        let mut b = err.borrow_mut();
603        b.props.insert("name".into(), Value::str("AbortError"));
604        b.props.insert(
605            "message".into(),
606            Value::str("Share was cancelled: no share target available in this environment"),
607        );
608    }
609    Ok(rejected_promise(it, Value::Object(err)))
610}
611
612/// `new EyeDropper()`(丸ごと未対応だった)。`.open()` メソッドのみを持つ
613/// オブジェクトを返す。
614pub(crate) fn eye_dropper_ctor(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
615    let o = Obj::plain();
616    o.borrow_mut().props.insert("open".into(), nv("EyeDropper.open", eye_dropper_open));
617    Ok(Value::Object(o))
618}
619/// `EyeDropper.prototype.open()`。実際のピッカー UI が無いため、ユーザーが
620/// ピッカーをキャンセルした場合と同じ仕様どおりの `AbortError` で常に拒否
621/// する(色を拾えていないのに成功を偽装しない)。
622pub(crate) fn eye_dropper_open(it: &mut Interp, _this: Value, _a: &[Value]) -> Result<Value, Value> {
623    let err = Obj::plain();
624    {
625        let mut b = err.borrow_mut();
626        b.props.insert("name".into(), Value::str("AbortError"));
627        b.props.insert(
628            "message".into(),
629            Value::str("The color picker was cancelled: no picker UI available in this environment"),
630        );
631    }
632    Ok(rejected_promise(it, Value::Object(err)))
633}
634
635/// `navigator.vibrate(pattern)`(Vibration API。丸ごと未対応だった)。仕様上
636/// 戻り値は「リクエストを受理したか」のみを表しハードウェアの有無とは無関係
637/// なため、常に `true` を返す簡略実装(実際に振動は発生しない)。
638pub(crate) fn navigator_vibrate(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
639    Ok(Value::Bool(true))
640}
641
642/// `navigator.getBattery()`(Battery Status API。丸ごと未対応だった)。この
643/// OS のターゲット(Raspberry Pi)はバッテリー非搭載で常時 AC 電源動作の
644/// ため、「常時満充電で給電中」を返す簡略実装(`levelchange` 等のイベントは
645/// 対象外)。
646/// `navigator.locks.request(name, options?, callback)`(Web Locks API。丸ごと
647/// 未対応だった)。`options` は省略可能なため、実際に呼び出し可能な最後の
648/// 引数をコールバックとして扱う。コールバックへ渡す `Lock` は仕様の必須
649/// プロパティ(`name`/`mode`)のみを持つ簡略オブジェクト。
650pub(crate) fn navigator_locks_request(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
651    let cb = if matches!(arg(a, 2), Value::Object(o) if o.borrow().is_callable()) {
652        arg(a, 2)
653    } else {
654        arg(a, 1)
655    };
656    let name = arg(a, 0).to_js_string();
657    let lock = Obj::plain();
658    {
659        let mut b = lock.borrow_mut();
660        b.props.insert("name".into(), Value::str(name));
661        b.props.insert("mode".into(), Value::str("exclusive"));
662    }
663    let result = it.call_value(&cb, Value::Undefined, &[Value::Object(lock)])?;
664    Ok(resolved_promise(it, result))
665}
666/// `navigator.locks.query()`。実際の排他状態を追跡していないため、保持中/
667/// 待機中とも常に空のスナップショットを返す。
668pub(crate) fn navigator_locks_query(it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
669    let o = Obj::plain();
670    {
671        let mut b = o.borrow_mut();
672        b.props.insert("held".into(), Value::Object(Obj::array(alloc::vec![])));
673        b.props.insert("pending".into(), Value::Object(Obj::array(alloc::vec![])));
674    }
675    Ok(resolved_promise(it, Value::Object(o)))
676}
677pub(crate) fn navigator_get_battery(it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
678    let battery = Obj::plain();
679    {
680        let mut b = battery.borrow_mut();
681        b.props.insert("charging".into(), Value::Bool(true));
682        b.props.insert("chargingTime".into(), Value::Number(0.0));
683        b.props.insert("dischargingTime".into(), Value::Number(f64::INFINITY));
684        b.props.insert("level".into(), Value::Number(1.0));
685        b.props
686            .insert("addEventListener".into(), nv("addEventListener", dom_noop));
687        b.props
688            .insert("removeEventListener".into(), nv("removeEventListener", dom_noop));
689    }
690    Ok(resolved_promise(it, Value::Object(battery)))
691}
692
693/// fetch(url|Request, options?): options.method/body/headers に対応(GET/POST)。
694/// AtmOS の HTTP は同期なので、その場で取得し解決済み Promise を返す。
695pub(crate) fn fetch(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
696    let first = arg(a, 0);
697    // `fetch(new Request(url, opts))`(URL 文字列を渡す通常形と並ぶもう一方の
698    // 定番の呼び出し形)が丸ごと未対応で、`Request` オブジェクトを渡すと
699    // `to_js_string()` が `[object Object]` になり不正な URL 扱いになっていた。
700    let is_request_like = matches!(&first, Value::Object(o) if o.borrow().props.contains_key("url"));
701    let url = if is_request_like {
702        obj_prop(&first, "url").map(|v| v.to_js_string()).unwrap_or_default()
703    } else {
704        first.to_js_string()
705    };
706    let opts = arg(a, 1);
707    let method = obj_prop(&opts, "method")
708        .or_else(|| if is_request_like { obj_prop(&first, "method") } else { None })
709        .map(|v| v.to_js_string())
710        .unwrap_or_else(|| String::from("GET"));
711    let body = obj_prop(&opts, "body")
712        .or_else(|| if is_request_like { obj_prop(&first, "_body") } else { None })
713        .map(|v| body_to_string(&v))
714        .unwrap_or_default();
715    let headers = obj_prop(&opts, "headers")
716        .or_else(|| if is_request_like { obj_prop(&first, "headers") } else { None })
717        .unwrap_or(Value::Undefined);
718    let content_type =
719        headers_content_type(&headers).unwrap_or_else(|| String::from("text/plain;charset=UTF-8"));
720    // `fetch(url, {signal})` が丸ごと未対応で、`AbortSignal` を渡しても一切見ずに
721    // 常にリクエストを実行してしまっていた。この処理系の HTTP は同期実行のため
722    // 「実行中に中断する」ことはできないが、仕様どおり「呼び出し時点で既に
723    // aborted な signal なら即座に AbortError で reject し、実際のリクエストは
724    // 一切送らない」チェックは実装できる(`fetch` 前のバリデーション失敗を
725    // `AbortSignal.abort()` で早期中断する定番パターンとも整合する)。
726    let signal_opt = obj_prop(&opts, "signal")
727        .or_else(|| if is_request_like { obj_prop(&first, "signal") } else { None });
728    if let Some(signal) = signal_opt {
729        let already_aborted = matches!(&signal, Value::Object(o) if matches!(o.borrow().props.get("aborted"), Some(Value::Bool(true))));
730        if already_aborted {
731            let reason = match &signal {
732                Value::Object(o) => o.borrow().props.get("reason").cloned().unwrap_or(Value::Undefined),
733                _ => Value::Undefined,
734            };
735            return Ok(rejected_promise(it, reason));
736        }
737    }
738    let base = it.base_url.clone();
739    let resolved_url = resolve_url(&base, &url);
740    match do_http_request(&base, &url, &method, &body, &content_type) {
741        Ok((status, body)) => {
742            Ok(resolved_promise(it, make_response_with_url(status, body, resolved_url)))
743        }
744        Err(msg) => Ok(rejected_promise(it, it.error(format!("fetch: {}", msg)))),
745    }
746}
747
748/// XMLHttpRequest: AtmOS の HTTP は同期なので send() で即取得し readyState=4 まで進め、
749/// onreadystatechange → onload(またはonerror) を同期発火する。
750pub(crate) fn xhr_ctor(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
751    let o = Obj::plain();
752    {
753        let mut b = o.borrow_mut();
754        b.props.insert("readyState".into(), Value::Number(0.0));
755        b.props.insert("status".into(), Value::Number(0.0));
756        b.props.insert("statusText".into(), Value::str(""));
757        b.props.insert("responseText".into(), Value::str(""));
758        b.props.insert("response".into(), Value::str(""));
759        b.props.insert("open".into(), nv("open", xhr_open));
760        b.props.insert("send".into(), nv("send", xhr_send));
761        b.props.insert(
762            "setRequestHeader".into(),
763            nv("setRequestHeader", xhr_set_request_header),
764        );
765        b.props.insert("abort".into(), nv("abort", xhr_noop));
766        b.props.insert(
767            "getAllResponseHeaders".into(),
768            nv("getAllResponseHeaders", xhr_empty_str),
769        );
770        // `getResponseHeader(name)` が丸ごと未対応で「関数ではない」の TypeError
771        // になっていた。`getAllResponseHeaders` と同じ理由(この処理系の HTTP
772        // クライアントは応答ヘッダを捕捉していない)で常に `null`(仕様上、該当
773        // ヘッダが無い場合の戻り値)を返す。
774        b.props.insert(
775            "getResponseHeader".into(),
776            nv("getResponseHeader", xhr_get_response_header),
777        );
778        b.props.insert(
779            "addEventListener".into(),
780            nv("addEventListener", xhr_add_event_listener),
781        );
782        // `removeEventListener` が丸ごと未対応で、`xhr.removeEventListener(...)`
783        // が「関数ではない」という TypeError になっていた(`AbortSignal` の
784        // 対応済みリスナ削除と同じパターンで対応する)。
785        b.props.insert(
786            "removeEventListener".into(),
787            nv("removeEventListener", xhr_remove_event_listener),
788        );
789    }
790    Ok(Value::Object(o))
791}
792
793pub(crate) fn xhr_noop(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
794    Ok(Value::Undefined)
795}
796
797/// setRequestHeader(name, value): Content-Type のみ保持(POST のボディ送信に使用)。
798pub(crate) fn xhr_set_request_header(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
799    if let Value::Object(o) = &this {
800        if arg(a, 0)
801            .to_js_string()
802            .eq_ignore_ascii_case("content-type")
803        {
804            o.borrow_mut()
805                .props
806                .insert("_content_type".into(), Value::str(arg(a, 1).to_js_string()));
807        }
808    }
809    Ok(Value::Undefined)
810}
811
812pub(crate) fn xhr_empty_str(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
813    Ok(Value::str(""))
814}
815pub(crate) fn xhr_get_response_header(_it: &mut Interp, _t: Value, _a: &[Value]) -> Result<Value, Value> {
816    Ok(Value::Null)
817}
818
819/// addEventListener(type, fn) を on<type> プロパティへ写像(最後の登録が有効)。
820pub(crate) fn xhr_add_event_listener(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
821    if let Value::Object(o) = &this {
822        o.borrow_mut()
823            .props
824            .insert(format!("on{}", arg(a, 0).to_js_string()), arg(a, 1));
825    }
826    Ok(Value::Undefined)
827}
828/// removeEventListener(type, fn)。`AbortSignal` と同じ「`on<type>` に単一リスナを
829/// 格納する」簡略方式に合わせ、渡された関数が現在登録済みのものと同一参照の場合
830/// のみ削除する。
831pub(crate) fn xhr_remove_event_listener(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
832    if let Value::Object(o) = &this {
833        let key = format!("on{}", arg(a, 0).to_js_string());
834        let target = arg(a, 1);
835        let matches = match (o.borrow().props.get(&key), &target) {
836            (Some(Value::Object(stored)), Value::Object(t)) => Rc::ptr_eq(stored, t),
837            _ => false,
838        };
839        if matches {
840            o.borrow_mut().props.shift_remove(&key);
841        }
842    }
843    Ok(Value::Undefined)
844}
845
846pub(crate) fn xhr_open(_it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
847    if let Value::Object(o) = &this {
848        let mut b = o.borrow_mut();
849        b.props
850            .insert("_method".into(), Value::str(arg(a, 0).to_js_string()));
851        b.props
852            .insert("_url".into(), Value::str(arg(a, 1).to_js_string()));
853        b.props.insert("readyState".into(), Value::Number(1.0));
854    }
855    Ok(Value::Undefined)
856}
857
858pub(crate) fn xhr_send(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
859    let o = match &this {
860        Value::Object(o) => o.clone(),
861        _ => return Ok(Value::Undefined),
862    };
863    let (url, method, content_type) = {
864        let b = o.borrow();
865        (
866            b.props
867                .get("_url")
868                .map(|v| v.to_js_string())
869                .unwrap_or_default(),
870            b.props
871                .get("_method")
872                .map(|v| v.to_js_string())
873                .unwrap_or_else(|| String::from("GET")),
874            b.props
875                .get("_content_type")
876                .map(|v| v.to_js_string())
877                .unwrap_or_else(|| String::from("text/plain;charset=UTF-8")),
878        )
879    };
880    // send(body): undefined/null は空ボディ("undefined" 文字列化を防ぐ)。
881    let req_body = match arg(a, 0) {
882        Value::Undefined | Value::Null => String::new(),
883        other => other.to_js_string(),
884    };
885    let base = it.base_url.clone();
886    let (status, body, ok) = match do_http_request(&base, &url, &method, &req_body, &content_type) {
887        Ok((s, b)) => (s, b, true),
888        Err(e) => {
889            // 失敗理由を捨てない。従来は `Err(_)` で握り潰しており、
890            // 取得が失敗しても `status=0` になるだけで原因が残らなかった。
891            crate::warn!("[XHR] 取得失敗 url={:?} 理由={}", url, e);
892            (0u16, String::new(), false)
893        }
894    };
895    {
896        let mut b = o.borrow_mut();
897        b.props
898            .insert("status".into(), Value::Number(status as f64));
899        // `statusText` はコンストラクタで空文字列に初期化されたきり、`send()` 完了後も
900        // 一切更新されないバグだった(`Response.prototype.statusText` と同種の欠落。
901        // `xhr.status`/`xhr.statusText` を併記するエラーログの定番パターンで参照される)。
902        b.props
903            .insert("statusText".into(), Value::str(http_status_text(status)));
904        b.props
905            .insert("responseText".into(), Value::str(body.clone()));
906        b.props.insert("response".into(), Value::str(body));
907        b.props.insert("readyState".into(), Value::Number(4.0));
908    }
909    let onrsc = o.borrow().props.get("onreadystatechange").cloned();
910    if let Some(f) = onrsc.and_then(callable_or_none) {
911        it.call_listener(&f, this.clone(), &[], "XHR onreadystatechange");
912    }
913    let cb = o
914        .borrow()
915        .props
916        .get(if ok { "onload" } else { "onerror" })
917        .cloned();
918    if let Some(f) = cb.and_then(callable_or_none) {
919        let ctx = if ok { "XHR onload" } else { "XHR onerror" };
920        it.call_listener(&f, this.clone(), &[], ctx);
921    }
922    Ok(Value::Undefined)
923}
924
925/// Promise.all: マイクロタスクを駆動して各入力を解決し、結果配列に集約する。
926pub(crate) fn promise_all_static(it: &mut Interp, _t: Value, a: &[Value]) -> Result<Value, Value> {
927    let items = it.iter_to_vec(&arg(a, 0));
928    let result = new_pending();
929    // 保留中の Promise を同期的に解決まで進める。
930    it.run_microtasks();
931    let mut out: Vec<Value> = Vec::new();
932    let mut rejected: Option<Value> = None;
933    for item in items {
934        // 汎用 thenable(`.then` を持つが本物の Promise ではないオブジェクト)が
935        // 「非 Promise」としてそのまま結果配列に混入するバグだった(`Promise.resolve`/
936        // `await` 単体では既に同化に対応済みだったのに `Promise.all` だけ非対称
937        // だった)。`allSettled`/`race`/`any` と共通の `resolve_maybe_thenable` を使う。
938        match resolve_maybe_thenable(it, &item) {
939            Some(s) => {
940                let b = s.borrow();
941                match b.status {
942                    PromiseStatus::Fulfilled => out.push(b.value.clone()),
943                    PromiseStatus::Rejected => {
944                        rejected = Some(b.value.clone());
945                        break;
946                    }
947                    PromiseStatus::Pending => out.push(Value::Undefined),
948                }
949            }
950            None => out.push(item), // 非 Promise・非 thenable はそのまま。
951        }
952    }
953    match rejected {
954        Some(e) => it.promise_reject(&result, e),
955        None => it.promise_resolve(&result, Value::Object(Obj::array(out))),
956    }
957    Ok(Value::Object(Obj::promise(result)))
958}
959
960/// `then`/`catch`/`finally` メソッド(interp の get_property から返す)。
961pub fn promise_method(key: &str) -> Value {
962    match key {
963        "then" => nv("Promise.then", promise_then_m),
964        "catch" => nv("Promise.catch", promise_catch_m),
965        "finally" => nv("Promise.finally", promise_finally_m),
966        _ => Value::Undefined,
967    }
968}
969
970pub(crate) fn promise_then_m(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
971    if let Some(s) = this_promise_state(&this) {
972        let on_f = callable_or_none(arg(a, 0));
973        let on_r = callable_or_none(arg(a, 1));
974        return Ok(it.promise_then(&s, on_f, on_r));
975    }
976    Ok(Value::Undefined)
977}
978
979pub(crate) fn promise_catch_m(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
980    if let Some(s) = this_promise_state(&this) {
981        let on_r = callable_or_none(arg(a, 0));
982        return Ok(it.promise_then(&s, None, on_r));
983    }
984    Ok(Value::Undefined)
985}
986
987/// `Promise.prototype.finally(cb)`。以前は `cb` を fulfill/reject 双方で副作用として
988/// 実行するだけで戻り値/例外を完全に無視していた。仕様上は `cb` が例外を投げる、
989/// または reject する Promise を返した場合、その結果が元の settle を上書きすべき
990/// (`Promise.resolve(1).finally(() => { throw 'x' })` は `1` ではなく `'x'` で
991/// reject すべき)で、これが `cb` に副作用以上の意味が無いという誤った前提の
992/// 静かな破壊バグだった。ネイティブ関数はクロージャで `cb`/`this` を束縛できない
993/// ため(`AbortSignal.any` 等と同じ制約)、グローバルの一時変数経由で渡した上で
994/// `eval_source` に組み立てた JS(仕様の `finally` 展開そのもの)を実行する。
995pub(crate) fn promise_finally_m(it: &mut Interp, this: Value, a: &[Value]) -> Result<Value, Value> {
996    let Some(s) = this_promise_state(&this) else {
997        return Ok(Value::Undefined);
998    };
999    let cb = arg(a, 0);
1000    if callable_or_none(cb.clone()).is_none() {
1001        // cb が呼び出し不能なら仕様上ただの `then()` と同じ(副作用なし)。
1002        return Ok(it.promise_then(&s, None, None));
1003    }
1004    it.global
1005        .borrow_mut()
1006        .vars
1007        .insert("__finally_promise".into(), this);
1008    it.global.borrow_mut().vars.insert("__finally_cb".into(), cb);
1009    let src = "(function(){ \
1010        var p = __finally_promise; \
1011        var cb = __finally_cb; \
1012        return p.then( \
1013            function(v){ return Promise.resolve(cb()).then(function(){ return v; }); }, \
1014            function(r){ return Promise.resolve(cb()).then(function(){ throw r; }); } \
1015        ); \
1016    })()";
1017    let result = it.eval_source(src);
1018    it.global.borrow_mut().vars.remove("__finally_promise");
1019    it.global.borrow_mut().vars.remove("__finally_cb");
1020    result
1021}
1022