Skip to main content

atmos/os_lib/js/interp/
properties.rs

1// 分割: interp.rs の `impl Interp` から機械的に移動(2026-07-16 リファクタ フェーズ5)。
2// ロジック不変。メソッド可視性のみ pub(crate) へ昇格(別モジュールの impl Interp
3// からの self 呼び出しのため)。impl ブロックは型 Interp に自動で結合する。
4use super::*;
5
6
7impl Interp {
8    /// `key in obj` 用: 自身 props → プロトタイプ連鎖を辿って存在判定。
9    /// 配列の length / 数値インデックスも考慮する。
10    // 循環プロトタイプ連鎖で再帰が無限に深くならないよう、他の連鎖走査と同じ上限
11    // ガードを使った反復実装にしている(以前の素朴な再帰は無限再帰=スタック
12    // オーバーフローの危険があった)。
13    pub(crate) fn has_property_chain(&self, o: &ObjRef, key: &str) -> bool {
14        let mut cur = o.clone();
15        let mut guard = 0u32;
16        loop {
17            let b = cur.borrow();
18            if let ObjKind::Array(items) = &b.kind {
19                if key == "length" {
20                    return true;
21                }
22                if let Ok(idx) = key.parse::<usize>() {
23                    return idx < items.len() && !b.is_hole(idx);
24                }
25            }
26            // `accessors`(getter/setter 専用プロパティ)は `props` とは別マップで管理されて
27            // いるため、`props.contains_key` だけでは `{get x(){}}` のような accessor 限定の
28            // プロパティが `in` 演算子で「無い」ことになるバグがあった。
29            if b.props.contains_key(key) || b.accessors.contains_key(key) {
30                return true;
31            }
32            guard += 1;
33            if guard > 1000 {
34                return false;
35            }
36            let next = b.proto.clone();
37            let null_proto = b.null_proto;
38            drop(b);
39            match next {
40                Some(p) => cur = p,
41                // 連鎖の終端(組込みの prototype は実体から動的に解決するので、ここで補う)。
42                // 【2026-10-20】`'constructor' in {}` が false になっていた。Polymer の
43                // `ybe`(`if(!(n in B5f))` で constructor のコピーを除外する)が prototype の
44                // constructor を上書きし、YouTube の ytd-app の起動が `finalize is not a function` で止まった。
45                None => {
46                    if null_proto {
47                        return false;
48                    }
49                    if matches!(key, "constructor" | "__proto__") {
50                        return true;
51                    }
52                    if o.borrow().is_callable() && matches!(key, "call" | "apply" | "bind" | "name" | "length") {
53                        return true;
54                    }
55                    return !matches!(super::super::builtins::object_get(o, key), Value::Undefined);
56                }
57            }
58        }
59    }
60
61    /// `o` がグローバルオブジェクト(`window`)か。
62    pub(crate) fn is_window_obj(&self, o: &crate::os_lib::js::value::ObjRef) -> bool {
63        matches!(self.global.borrow().vars.get("window"), Some(Value::Object(w)) if alloc::rc::Rc::ptr_eq(w, o))
64    }
65
66    /// `<video>`/`<audio>` が再生スレッドの対象(src が一致)なら、その再生状態。
67    pub(crate) fn media_status(&self, idx: usize) -> Option<crate::os_lib::mse::PlaybackStatus> {
68        let src = self.dom.borrow().get_attr(idx, "src")?;
69        let st = crate::os_lib::mse::PLAYBACK.lock().clone();
70        (!src.is_empty() && st.url == src).then_some(st)
71    }
72
73    /// MSE の要素の長さ(秒): JS が設定した `mediaSource.duration`、無ければ buffered の終わり。
74    pub(crate) fn media_source_duration(&self, src: &str) -> Option<f64> {
75        if !src.starts_with("blob:mediasource-") {
76            return None;
77        }
78        let set = match self.global.borrow().vars.get("__media_source_urls") {
79            Some(Value::Object(r)) => match r.borrow().props.get(src) {
80                Some(Value::Object(ms)) => ms.borrow().props.get("duration").map(|v| v.to_number()),
81                _ => None,
82            },
83            _ => None,
84        };
85        match set {
86            Some(d) if d.is_finite() && d > 0.0 => Some(d),
87            _ => super::super::builtins::media_source_buffered_end(src),
88        }
89    }
90
91    /// `href`/`src`/`action` IDL プロパティ用: 現在のページURL基準で相対URLを
92    /// 絶対URLへ解決する(`JsRuntime::new_interp` の base_url 解決と同じ真実源)。
93    pub(crate) fn resolve_url_against_page(&self, raw: &str) -> String {
94        if raw.is_empty() {
95            return String::new();
96        }
97        let base = super::super::builtins::location_href(&self.global)
98            .filter(|s| !s.is_empty())
99            .unwrap_or_else(|| self.base_url.clone());
100        if base.is_empty() {
101            String::from(raw)
102        } else {
103            super::super::builtins::resolve_url(&base, raw)
104        }
105    }
106
107    /// `recv.key` を読む。`recv` が undefined/null のときの例外に、その値を生んだ式
108    /// (`obj_expr`)を `stack` の先頭行の直後へ添える(2026-09-26。どの式が undefined
109    /// だったか分からず、ミニファイされたライブラリの不具合を追えなかった)。
110    pub fn get_property_of_expr(
111        &mut self,
112        recv: &Value,
113        key: &str,
114        obj_expr: &crate::os_lib::js::ast::Expression,
115    ) -> EvalResult {
116        let r = self.get_property(recv, key);
117        if let (Err(Value::Object(o)), Value::Undefined | Value::Null) = (&r, recv) {
118            let desc = describe_expr(obj_expr);
119            let stack = o.borrow().props.get("stack").map(|v| v.to_js_string()).unwrap_or_default();
120            let mut lines = stack.splitn(2, '\n');
121            let first = lines.next().unwrap_or("");
122            let rest = lines.next();
123            let mut ns = alloc::format!("{first}\n    (undefined だった式: {desc})");
124            if let Some(rest) = rest {
125                ns.push('\n');
126                ns.push_str(rest);
127            }
128            o.borrow_mut().props.insert(String::from("stack"), Value::str(ns));
129        }
130        r
131    }
132
133    pub fn get_property(&mut self, recv: &Value, key: &str) -> EvalResult {
134        if key == "readyState" { // TMPRS
135            if let Value::Object(o) = recv {
136                if matches!(&o.borrow().kind, ObjKind::Host(t) if t == "document") {
137                    static N: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
138                    if N.fetch_add(1, core::sync::atomic::Ordering::Relaxed) < 40 {
139                        let v = o.borrow().props.get("readyState").map(|v| v.to_js_string()).unwrap_or_default();
140                        crate::warn!("[TMPRS] read readyState={} stack={:?}", v, self.call_names.iter().rev().take(4).collect::<Vec<_>>());
141                    }
142                }
143            }
144        }
145        match recv {
146            Value::Undefined | Value::Null if key == "apply" && self.tmp_bodies.last().is_some_and(|b| b.is_some()) => { // TMPAPPLY
147                static N: core::sync::atomic::AtomicUsize = core::sync::atomic::AtomicUsize::new(0);
148                if N.fetch_add(1, core::sync::atomic::Ordering::Relaxed) < 4 {
149                    for (k, b) in self.tmp_bodies.iter().rev().take(3).enumerate() {
150                        if let Some(b) = b {
151                            let st = b.stmts();
152                            let d = format!("{:?}", st.first());
153                            crate::warn!("[TMPAPPLY] frame{} name={:?} stmts={} first={}", k, self.call_names.get(self.call_names.len() - 1 - k), st.len(), d.chars().take(700).collect::<String>());
154                        }
155                    }
156                }
157                Err(self.throw(format!("Cannot read properties of {} (reading '{}')", recv.to_js_string(), key)))
158            }
159            Value::Undefined | Value::Null => Err(self.throw(format!(
160                "Cannot read properties of {} (reading '{}')",
161                recv.to_js_string(),
162                key
163            ))),
164            Value::Str(_) | Value::Number(_) | Value::BigInt(_) | Value::Bool(_) => {
165                let (v, ctor) = match recv {
166                    Value::Str(s) => (super::super::builtins::string_get(s, key), "String"),
167                    Value::Number(n) => (super::super::builtins::number_get(*n, key), "Number"),
168                    Value::BigInt(b) => (super::super::builtins::bigint_get(b, key), "BigInt"),
169                    Value::Bool(b) => (super::super::builtins::boolean_get(*b, key), "Boolean"),
170                    _ => (Value::Undefined, ""),
171                };
172                if !matches!(v, Value::Undefined) {
173                    return Ok(v);
174                }
175                // 組込みに無い名前は `String.prototype` 等(ポリフィルで足されたメソッドを含む)、
176                // その先の `Object.prototype`(`hasOwnProperty` 等)から引く。【2026-10-17】
177                // `false.hasOwnProperty(k)`(YouTube の `(!a).hasOwnProperty(c)`)が落ちていた。
178                match super::super::builtins::global_ctor_proto(self, ctor) {
179                    Some(p) => self.get_property(&Value::Object(p), key),
180                    None => Ok(Value::Undefined),
181                }
182            }
183            Value::Object(o) => {
184                // Proxy: get トラップがあれば呼ぶ。無ければ target にフォワード。
185                let proxy = match &o.borrow().kind {
186                    ObjKind::Proxy { target, handler } => Some((target.clone(), handler.clone())),
187                    _ => None,
188                };
189                if let Some((target, handler)) = proxy {
190                    let trap = handler.borrow().props.get("get").cloned();
191                    if let Some(trap) =
192                        trap.filter(|t| matches!(t, Value::Object(f) if f.borrow().is_callable()))
193                    {
194                        let args = [Value::Object(target), Value::str(key), recv.clone()];
195                        return self.call_value(&trap, Value::Object(handler), &args);
196                    }
197                    return self.get_property(&Value::Object(target), key);
198                }
199                // グローバルオブジェクト: `window.x` は同じ名前のグローバル変数(スクリプトの
200                // トップレベルの `var`・関数宣言・組み込み)でもある。
201                // 【2026-10-17】従来は `window` を普通のオブジェクトとして持ち、`var ytcfg = …;
202                // window.ytcfg.set(…)`(YouTube の視聴ページ)が undefined になっていた。
203                // 逆方向(素の `x` → `window.x`)は `eval_expr` の識別子解決が見る。
204                // ネイティブのコンストラクタ(組み込み関数)の `prototype` を初回参照時に用意する。
205                // 【2026-10-17】多くの組み込みが `prototype` を持たず、ポリフィルの
206                // `if (!X.prototype.entries)` 等が「undefined の 'entries'」で落ちていた(YouTube)。
207                if key == "prototype"
208                    && matches!(o.borrow().kind, ObjKind::Native { .. })
209                    && !o.borrow().props.contains_key("prototype")
210                {
211                    let proto = Obj::plain();
212                    proto.borrow_mut().props.insert("constructor".into(), Value::Object(o.clone()));
213                    o.borrow_mut().props.insert("prototype".into(), Value::Object(proto.clone()));
214                    return Ok(Value::Object(proto));
215                }
216                if self.is_window_obj(o) && !o.borrow().props.contains_key(key) {
217                    if let Some(v) = self.global.borrow().vars.get(key).cloned() {
218                        return Ok(v);
219                    }
220                }
221                // `obj.__proto__`(Annex B のレガシーアクセサだが、`{...obj, __proto__:
222                // proto}` や `obj.__proto__ = null` という実務コードで極めて広く
223                // 使われるイディオム)が丸ごと未対応だった。`Object.getPrototypeOf`
224                // と同じ意味論(`Obj::proto` フィールドをそのまま読む)。ただし
225                // `{['__proto__']: v}`(算出キー)で作られた通常の own データ
226                // プロパティが既にある場合はそちらを優先する(そちらは特別扱い
227                // されない普通のプロパティのため)。
228                if key == "__proto__" && !o.borrow().props.contains_key("__proto__") {
229                    let dom_idx = match o.borrow().kind {
230                        ObjKind::DomElement(i) if o.borrow().proto.is_none() => Some(i),
231                        _ => None,
232                    };
233                    if let Some(i) = dom_idx {
234                        return Ok(self.get_dom_node_proto(i).map(Value::Object).unwrap_or(Value::Null));
235                    }
236                    return Ok(match o.borrow().proto.clone() {
237                        Some(p) => Value::Object(p),
238                        None => Value::Null,
239                    });
240                }
241                // RadioNodeList.prototype.value(HTML Standard §4.10.21.1 getter)。
242                if key == "value" && o.borrow().props.get("_is_radio_nodelist").map(|v| v.truthy()).unwrap_or(false) {
243                    if let ObjKind::Array(ref items) = o.borrow().kind {
244                        let dom = self.dom.borrow();
245                        for item in items {
246                            if let Value::Object(item_obj) = item {
247                                if let ObjKind::DomElement(idx) = item_obj.borrow().kind {
248                                    let is_radio = dom.nodes.get(idx).map(|n| n.tag == "input").unwrap_or(false)
249                                        && dom.get_attr(idx, "type").as_deref() == Some("radio");
250                                    if is_radio && dom.get_attr(idx, "checked").is_some() {
251                                        let v = dom.get_attr(idx, "value").unwrap_or_else(|| String::from("on"));
252                                        return Ok(Value::str(v));
253                                    }
254                                }
255                            }
256                        }
257                    }
258                    return Ok(Value::str(""));
259                }
260                if key == "selectedIndex" {
261                    if let Some(Value::Number(select_idx)) = o.borrow().props.get("_select_idx") {
262                        return Ok(Value::Number(self.dom.borrow().select_selected_index(*select_idx as usize) as f64));
263                    }
264                }
265                // DOM プロキシ(要素ハンドル / style / classList)の動的プロパティ。
266                let dom_kind = {
267                    match &o.borrow().kind {
268                        ObjKind::DomElement(i) => Some(DomDisp::Element(*i)),
269                        ObjKind::Host(t)
270                            if t.starts_with("style:")
271                                || t.starts_with("classList:")
272                                || t.starts_with("dataset:")
273                                || t.starts_with("computed:")
274                                || t.starts_with("stylesheet:") =>
275                        {
276                            Some(DomDisp::Host(t.clone()))
277                        }
278                        _ => None,
279                    }
280                };
281                if let Some(d) = dom_kind {
282                    return Ok(self.dom_get_property(d, key));
283                }
284                // localStorage / sessionStorage プロキシ。
285                let storage_tag = {
286                    match &o.borrow().kind {
287                        ObjKind::Host(t) if t.starts_with("storage:") => Some(t.clone()),
288                        _ => None,
289                    }
290                };
291                if let Some(t) = storage_tag {
292                    return Ok(super::super::builtins::storage_get_prop(&t, key));
293                }
294                // `entries`/`keys`/`values` が返す軽量イテレータ(`make_iterator()`。
295                // `Obj::host("iterator")` + 内部 `_items`/`_pos` で近似)の `next`/
296                // `Symbol.iterator` メソッド。
297                let is_iterator = matches!(&o.borrow().kind, ObjKind::Host(t) if t == "iterator");
298                if is_iterator {
299                    // ES2025 Iterator Helpers(`map`/`filter`/`take`/`drop`/`toArray`/
300                    // `forEach`/`reduce`)が丸ごと未対応だった。この軽量イテレータ表現の
301                    // 上に、残り要素をまとめて処理する即時評価版として実装(詳細は
302                    // `iterator_remaining()` のコメント参照)。
303                    return Ok(match key {
304                        "next" => {
305                            Value::Object(Obj::native("next", super::super::builtins::iterator_next))
306                        }
307                        "Symbol(Symbol.iterator)" => Value::Object(Obj::native(
308                            "[Symbol.iterator]",
309                            super::super::builtins::iterator_self,
310                        )),
311                        "map" => Value::Object(Obj::native("map", super::super::builtins::iterator_map)),
312                        "filter" => {
313                            Value::Object(Obj::native("filter", super::super::builtins::iterator_filter))
314                        }
315                        "take" => {
316                            Value::Object(Obj::native("take", super::super::builtins::iterator_take))
317                        }
318                        "drop" => {
319                            Value::Object(Obj::native("drop", super::super::builtins::iterator_drop))
320                        }
321                        "toArray" => Value::Object(Obj::native(
322                            "toArray",
323                            super::super::builtins::iterator_to_array,
324                        )),
325                        "forEach" => Value::Object(Obj::native(
326                            "forEach",
327                            super::super::builtins::iterator_for_each,
328                        )),
329                        "reduce" => {
330                            Value::Object(Obj::native("reduce", super::super::builtins::iterator_reduce))
331                        }
332                        // `some`/`every`/`find`(同じ ES2025 Iterator Helpers 提案に
333                        // 含まれる残り3つが丸ごと未対応だった)。
334                        "some" => Value::Object(Obj::native("some", super::super::builtins::iterator_some)),
335                        "every" => {
336                            Value::Object(Obj::native("every", super::super::builtins::iterator_every))
337                        }
338                        "find" => Value::Object(Obj::native("find", super::super::builtins::iterator_find)),
339                        // `flatMap`(ES2025 Iterator Helpers 提案の最後の1つが丸ごと
340                        // 未対応だった)。
341                        "flatMap" => {
342                            Value::Object(Obj::native("flatMap", super::super::builtins::iterator_flat_map))
343                        }
344                        _ => Value::Undefined,
345                    });
346                }
347                // document.body / head / documentElement → 要素ハンドル。
348                let is_document = matches!(&o.borrow().kind, ObjKind::Host(t) if t == "document");
349                if is_document {
350                    // DOMParser 等で生成された独立ドキュメント(`_root_idx` を保持)の場合、
351                    // グローバル DOM(`self.dom`)の body/head/contentType ではなく、
352                    // 自身が保持する props(contentType, documentElement, querySelector 等)を最優先する。
353                    if o.borrow().props.contains_key("_root_idx") {
354                        if let Some(prop_val) = o.borrow().props.get(key) {
355                            return Ok(prop_val.clone());
356                        }
357                    }
358                    let sel = match key {
359                        "body" => Some("body"),
360                        "head" => Some("head"),
361                        "documentElement" => Some("html"),
362                        // `document.scrollingElement`(HTML5。丸ごと未対応だった)。
363                        // この処理系は常に標準準拠モード(後述の `compatMode`
364                        // 参照)で動作するため、仕様どおり `documentElement`
365                        // (`<html>`)と同じ要素を返す簡略実装(quirks モードでの
366                        // `<body>` フォールバックは対象外)。
367                        "scrollingElement" => Some("html"),
368                        // `document.firstElementChild`/`.lastElementChild`
369                        // (`ParentNode`ミックスインが`Document`にも丸ごと
370                        // 未配線だった。2026-07-17 発見)。整形式の文書は
371                        // ルート要素`<html>`を唯一の要素子として持つため、
372                        // `documentElement`と同じ要素を返す簡略実装。
373                        "firstElementChild" | "lastElementChild" => Some("html"),
374                        _ => None,
375                    };
376                    if let Some(sel) = sel {
377                        if self.dom.borrow().nodes.is_empty() {
378                            self.dom.borrow_mut().ensure_default_document();
379                        }
380                        let idx = self.dom.borrow().query(sel);
381                        return Ok(match idx {
382                            Some(i) => Value::Object(Obj::dom(i)),
383                            None => Value::Null,
384                        });
385                    }
386                    // `document.children`/`.childElementCount`(同じ
387                    // `ParentNode`ミックスイン配線漏れ。`<html>`ルート要素
388                    // 1件のみを要素として含む簡略実装)。
389                    if key == "children" {
390                        let idx = self.dom.borrow().query("html");
391                        return Ok(Value::Object(Obj::array(match idx {
392                            Some(i) => alloc::vec![Value::Object(Obj::dom(i))],
393                            None => alloc::vec![],
394                        })));
395                    }
396                    if key == "childElementCount" {
397                        let idx = self.dom.borrow().query("html");
398                        return Ok(Value::Number(if idx.is_some() { 1.0 } else { 0.0 }));
399                    }
400                    // `document.activeElement`(HTML5。丸ごと未対応だった。常に
401                    // `null` 固定だったバグ)。`element.focus()`/`.blur()`(詳細は
402                    // `builtins::dom_focus`/`dom_blur` 参照)が更新する
403                    // `DomBridge::focused_idx` を真実源とし、何もフォーカスされて
404                    // いなければ仕様どおり `document.body` にフォールバックする。
405                    if key == "activeElement" {
406                        let focused = self.dom.borrow().focused_idx;
407                        let idx = focused.or_else(|| self.dom.borrow().query("body"));
408                        return Ok(match idx {
409                            Some(i) => Value::Object(Obj::dom(i)),
410                            None => Value::Null,
411                        });
412                    }
413                    if key == "styleSheets" {
414                        let dom = self.dom.borrow();
415                        let mut sheets = alloc::vec![];
416                        for (i, node) in dom.nodes.iter().enumerate() {
417                            if !node.is_text && node.tag == "style" {
418                                sheets.push(Value::Object(Obj::host(&format!("stylesheet:{}", i))));
419                            }
420                        }
421                        return Ok(Value::Object(Obj::array(sheets)));
422                    }
423                    // `document.title` が常に install 時の空文字列固定で、`<title>`
424                    // 要素の実際の中身や代入結果と一切連動していなかったバグ。`<title>`
425                    // 要素があればそれを真実源とし、無ければ(`<head>` 自体が無い場合の
426                    // フォールバックとして)代入時に保存した内部プロパティ `_title` を返す。
427                    if key == "title" {
428                        let title_idx = self.dom.borrow().query("title");
429                        let text = match title_idx {
430                            Some(i) => self.dom.borrow().get_text_content(i),
431                            None => o
432                                .borrow()
433                                .props
434                                .get("_title")
435                                .map(|v| v.to_js_string())
436                                .unwrap_or_default(),
437                        };
438                        return Ok(Value::str(text));
439                    }
440                    // `document.URL`/`document.documentURI`(DOM 標準。丸ごと未対応
441                    // だった)。この処理系に `<base>` タグ対応が無いため、常に
442                    // `location.href` と同一の値を返す簡略実装。
443                    if key == "URL" || key == "documentURI" {
444                        return Ok(Value::str(
445                            super::super::builtins::location_href(&self.global).unwrap_or_default(),
446                        ));
447                    }
448                    // `document.readyState`(HTML5。丸ごと未対応だった。`if
449                    // (document.readyState === 'complete') {...}` という定番の
450                    // フィーチャー検出イディオムで使われる)。この処理系は
451                    // ページ全体を同期的にパースしてから `<script>` を実行する
452                    // ため「loading」/「interactive」の中間状態を持たず、常に
453                    // `"complete"` を返す簡略実装。
454                    if key == "readyState" {
455                        return Ok(Value::str("complete"));
456                    }
457                    // `document.compatMode`(HTML5。丸ごと未対応だった。`if
458                    // (document.compatMode === 'CSS1Compat')` という定番の
459                    // 標準準拠モード判定イディオム)。この処理系に quirks モード
460                    // 自体が無く常に標準準拠として扱うため固定値を返す簡略実装。
461                    if key == "compatMode" {
462                        return Ok(Value::str("CSS1Compat"));
463                    }
464                    // `document.characterSet`(別名 `charset`/`inputEncoding`。
465                    // HTML5。丸ごと未対応だった)。この処理系は `<meta charset>`
466                    // の宣言値に関わらず常に UTF-8 として扱うため固定値を返す
467                    // 簡略実装。
468                    if key == "characterSet" || key == "charset" || key == "inputEncoding" {
469                        return Ok(Value::str("UTF-8"));
470                    }
471                    // `document.contentType`(HTML5。丸ごと未対応だった)。この
472                    // 処理系は常に HTML 文書として扱うため固定値を返す簡略実装。
473                    if key == "contentType" {
474                        return Ok(Value::str("text/html"));
475                    }
476                    // `document.cookie`(丸ごと未対応だった。詳細は
477                    // `builtins::cookie_string`/`cookie_set` 参照)。
478                    if key == "cookie" {
479                        return Ok(Value::str(super::super::builtins::cookie_string()));
480                    }
481                    // `document.referrer`(HTML5。丸ごと未対応だった。`if
482                    // (document.referrer) {...}` という定番のリファラ有無判定
483                    // イディオム)。この処理系にはページ遷移元を追跡する仕組みが
484                    // 無いため、常に空文字列(仕様上「リファラ無し」を表す値)を
485                    // 返す簡略実装。
486                    // 【2026-10-17】iframe の文書は読み込み時に送った `Referer` を返す
487                    // (`JsRuntime::set_referrer`)。それ以外は空文字列。
488                    if key == "referrer" {
489                        let r = self.global.borrow().vars.get(super::DOCUMENT_REFERRER_KEY).cloned();
490                        return Ok(r.unwrap_or_else(|| Value::str("")));
491                    }
492                    // `document.lastModified`(HTML5。丸ごと未対応だった)。実ファイル
493                    // の mtime を追跡する仕組みが無いため、`Date.now()` と同じ
494                    // `epoch_ms_now()` を仕様どおりの `"MM/DD/YYYY HH:MM:SS"` 形式へ
495                    // 整形して返す簡略実装。
496                    if key == "lastModified" {
497                        return Ok(Value::str(super::super::builtins::format_last_modified()));
498                    }
499                    // `document.domain`(HTML5 legacy。丸ごと未対応だった)。この
500                    // 処理系には同一オリジンポリシーの緩和機構自体が無いため、
501                    // `location.hostname` をそのまま返す読み取り専用近似とする
502                    // (代入は no-op のまま。`set_property` 側にも分岐を追加しない)。
503                    if key == "domain" {
504                        return Ok(Value::str(
505                            super::super::builtins::location_hostname(&self.global).unwrap_or_default(),
506                        ));
507                    }
508                    // Page Visibility API(`document.hidden`/`.visibilityState`)が
509                    // 丸ごと未対応だった。この処理系にはタブ/バックグラウンド化の
510                    // 概念自体が無く、ページは常に前面表示されているため、常に
511                    // 「表示中」固定値を返す簡略実装(`visibilitychange` イベントは
512                    // 発火する契機が無いため対象外のまま)。
513                    if key == "hidden" {
514                        return Ok(Value::Bool(false));
515                    }
516                    if key == "visibilityState" {
517                        return Ok(Value::str("visible"));
518                    }
519                    // `document.forms`/`.images`/`.scripts`(文書全体をタグ横断で
520                    // 集約する定番の HTMLCollection 群)が丸ごと未対応だった。既存の
521                    // `query_all`(接続済みノードのみを対象にする `querySelectorAll`
522                    // の実体)をそのまま再利用する。
523                    let coll_sel = match key {
524                        "forms" => Some("form"),
525                        "images" => Some("img"),
526                        "scripts" => Some("script"),
527                        // `document.embeds`/`.plugins`(丸ごと未対応だった。仕様上
528                        // 両者は同じ `<embed>` 集合を指すエイリアス)。2026-07-16 発見。
529                        "embeds" | "plugins" => Some("embed"),
530                        _ => None,
531                    };
532                    if let Some(sel) = coll_sel {
533                        let indices = self.dom.borrow().query_all(sel);
534                        return Ok(Value::Object(Obj::array(
535                            indices.into_iter().map(|i| Value::Object(Obj::dom(i))).collect(),
536                        )));
537                    }
538                    // `document.applets`(丸ごと未対応だった。Java アプレット廃止に
539                    // 伴い仕様上は常に空の `HTMLCollection` を返す固定のレガシー
540                    // スタブ。2026-07-16 発見)。
541                    if key == "applets" {
542                        return Ok(Value::Object(Obj::array(alloc::vec![])));
543                    }
544                    // `document.links`(仕様上 href 付きの a/area のみ)/`document.anchors`
545                    // (`name` 属性付きの a のみ)も同じ理由で丸ごと未対応だった。この
546                    // 処理系のセレクタには複合セレクタの OR 評価は無いが、`a[href]`/
547                    // `area[href]` という属性セレクタ自体は対応済みのため、2回の
548                    // `query_all` を結果マージ(node index はパース順=文書順なので
549                    // ソートで正しい順序になる)すれば OR 無しでも実現できる。
550                    if key == "links" {
551                        let dom = self.dom.borrow();
552                        let mut indices = dom.query_all("a[href]");
553                        indices.extend(dom.query_all("area[href]"));
554                        indices.sort_unstable();
555                        return Ok(Value::Object(Obj::array(
556                            indices.into_iter().map(|i| Value::Object(Obj::dom(i))).collect(),
557                        )));
558                    }
559                    if key == "anchors" {
560                        let indices = self.dom.borrow().query_all("a[name]");
561                        return Ok(Value::Object(Obj::array(
562                            indices.into_iter().map(|i| Value::Object(Obj::dom(i))).collect(),
563                        )));
564                    }
565                    // それ以外(getElementById 等のメソッド)は通常の props 解決へ。
566                }
567                // Map / Set の size とメソッド。
568                let mapset = {
569                    match &o.borrow().kind {
570                        ObjKind::MapObj(e) => Some((true, e.len())),
571                        ObjKind::SetObj(s) => Some((false, s.len())),
572                        _ => None,
573                    }
574                };
575                if let Some((is_map, len)) = mapset {
576                    if key == "size" {
577                        return Ok(Value::Number(len as f64));
578                    }
579                    return Ok(super::super::builtins::mapset_method(is_map, key));
580                }
581                // `URLSearchParams.prototype.size`(ES2023。エントリ数を返す)が
582                // 丸ごと未対応だった。`_is_usp` マーカー(`FormData` は同じ `_query`
583                // 内部表現を共有するが `size` は USP 固有の仕様のため、区別用に
584                // `url_search_params_ctor` でのみ立てる)を持つオブジェクトに限り、
585                // 都度クエリ文字列をパースしてエントリ数を返す(Map/Set の `size` と
586                // 違い専用 ObjKind が無い plain object 実装のため)。
587                if key == "size" && o.borrow().props.contains_key("_is_usp") {
588                    let n = super::super::builtins::usp_entry_count(&Value::Object(o.clone()));
589                    return Ok(Value::Number(n as f64));
590                }
591                // Promise のメソッド(then/catch/finally)。
592                let is_promise = matches!(&o.borrow().kind, ObjKind::PromiseObj(_));
593                if is_promise {
594                    return Ok(super::super::builtins::promise_method(key));
595                }
596                // Generator のメソッド(next/return)。
597                let is_gen = matches!(&o.borrow().kind, ObjKind::Generator(_));
598                if is_gen {
599                    if key == "Symbol(Symbol.iterator)" {
600                        return Ok(super::super::builtins::gen_self_iterator_fn());
601                    }
602                    if key == "Symbol(Symbol.asyncIterator)" {
603                        let is_async = if let ObjKind::Generator(g) = &o.borrow().kind {
604                            g.borrow().func.is_async
605                        } else {
606                            false
607                        };
608                        return Ok(if is_async {
609                            super::super::builtins::gen_self_iterator_fn()
610                        } else {
611                            Value::Undefined
612                        });
613                    }
614                    return Ok(super::super::builtins::generator_method(key));
615                }
616                // RegExp のプロパティ/メソッド。
617                // 組込みに無い名前は、自前のプロパティと原型の連鎖(下の共通処理)から引く。
618                // 【2026-10-19】XRegExp は正規表現に `re.xregexp = {…}` と覚えさせ、
619                // `re.__proto__ = XRegExp.prototype` で原型も差し替える。従来は組込み以外を
620                // 常に undefined にしており、kevlar_base(YouTube)で落ちていた。
621                let is_re = matches!(&o.borrow().kind, ObjKind::RegExpObj(_));
622                if is_re && !o.borrow().props.contains_key(key) && !o.borrow().accessors.contains_key(key) {
623                    let v = super::super::builtins::regexp_get(o, key);
624                    if !matches!(v, Value::Undefined) {
625                        return Ok(v);
626                    }
627                }
628                // Date のメソッド。
629                let is_date = matches!(&o.borrow().kind, ObjKind::DateObj(_));
630                if is_date {
631                    return Ok(super::super::builtins::date_get(key));
632                }
633                // 配列の length / 数値インデックス。
634                {
635                    let b = o.borrow();
636                    if let ObjKind::Array(items) = &b.kind {
637                        if key == "length" {
638                            return Ok(Value::Number(items.len() as f64));
639                        }
640                        if let Ok(idx) = key.parse::<usize>() {
641                            return Ok(items.get(idx).cloned().unwrap_or(Value::Undefined));
642                        }
643                    }
644                    if let ObjKind::Bytes(st) = &b.kind {
645                        if key == "length" {
646                            return Ok(Value::Number(st.len as f64));
647                        }
648                        if let Ok(idx) = key.parse::<usize>() {
649                            return Ok(st.get(idx).map(Value::Number).unwrap_or(Value::Undefined));
650                        }
651                    }
652                }
653                // 関数オブジェクトの call/apply/bind(Function.prototype 相当)。
654                if matches!(key, "call" | "apply" | "bind") && o.borrow().is_callable() {
655                    return Ok(super::super::builtins::function_method(key));
656                }
657                // 関数の `name`/`length`(ES1/ES3 以来の最も基礎的な関数メタデータ)。
658                // 以前は生成時に props へ一切設定しておらず、通常の props/proto 探索でも
659                // 見つからないため常に `undefined` になっていた。`length` は仕様どおり
660                // 「デフォルト値/rest が現れる前までの引数の個数」。
661                if key == "name" || key == "length" {
662                    // `Function.prototype.bind()` が返す関数(`ObjKind::Bound`)は
663                    // `.name`/`.length` を一切持たず、以前は `undefined` になっていた
664                    // バグだった。仕様どおり `.name` は `"bound " + 元の名前`、
665                    // `.length` は `元の length - 束縛済み引数の個数`(0未満にはならない)。
666                    let bound_info = match &o.borrow().kind {
667                        ObjKind::Bound { target, bound_args, .. } => {
668                            Some((target.clone(), bound_args.len()))
669                        }
670                        _ => None,
671                    };
672                    if let Some((target, bound_count)) = bound_info {
673                        return Ok(if key == "name" {
674                            let inner = self
675                                .get_property(&target, "name")
676                                .map(|v| v.to_js_string())
677                                .unwrap_or_default();
678                            Value::str(format!("bound {}", inner))
679                        } else {
680                            let inner_len = self
681                                .get_property(&target, "length")
682                                .map(|v| v.to_number() as usize)
683                                .unwrap_or(0);
684                            Value::Number(inner_len.saturating_sub(bound_count) as f64)
685                        });
686                    }
687                    let info = match &o.borrow().kind {
688                        ObjKind::Function(fd) => {
689                            let len = fd
690                                .params
691                                .iter()
692                                .take_while(|p| p.default.is_none() && !p.is_rest)
693                                .count();
694                            Some((fd.name.clone(), len))
695                        }
696                        ObjKind::Native { name, .. } => Some((name.clone(), 0usize)),
697                        _ => None,
698                    };
699                    if let Some((name, len)) = info {
700                        return Ok(if key == "name" {
701                            Value::str(name)
702                        } else {
703                            Value::Number(len as f64)
704                        });
705                    }
706                }
707                // 自身のプロパティ / アクセサ / プロトタイプ連鎖を辿って解決する。
708                // アクセサの getter は必ず元の recv(レシーバ)を this として呼ぶ。
709                // 以前はプロトタイプ連鎖を get_property の再帰で辿っていたため、
710                // 継承した getter(class の `get x(){}` 等)を呼ぶ際に this が
711                // プロトタイプ自身に化けてしまい、インスタンス側のフィールドを
712                // 正しく読めない(`this.r` 等が常に undefined になる)バグがあった。
713                // ここでは recv を固定したまま cur だけをプロトタイプ連鎖に沿って
714                // 進めるループに書き換えている。
715                let mut cur = o.clone();
716                // `Object.setPrototypeOf` は循環(`setPrototypeOf(a,a)` 等)を検証
717                // しないため、プロトタイプ連鎖に循環があるとこのループが無限になり得る
718                // (`Proxy` チェーン解決ループで見つかったのと同型のバグ)。他の連鎖
719                // 走査と同じ上限ガードで打ち切る。2026-07-11 に一度試みて「新しい
720                // ハング」に見えたため撤回したが、2026-07-12 にそれが QEMU 起動側の
721                // host タイミング変動による誤検知だったと判明したため再度追加する。
722                let mut guard = 0u32;
723                loop {
724                    let has_accessor_entry = cur.borrow().accessors.contains_key(key);
725                    if has_accessor_entry {
726                        // アクセサはデータプロパティより優先される(仕様通り)。
727                        // get が無ければ(set のみ定義)undefined を返して打ち切る。
728                        let getter = cur.borrow().accessors.get(key).and_then(|a| a.get.clone());
729                        return if let Some(g) = getter {
730                            self.call_value(&g, recv.clone(), &[])
731                        } else {
732                            Ok(Value::Undefined)
733                        };
734                    }
735                    let prop = cur.borrow().props.get(key).cloned();
736                    if let Some(v) = prop {
737                        return Ok(v);
738                    }
739                    guard += 1;
740                    if guard > 1000 {
741                        break;
742                    }
743                    let next = cur.borrow().proto.clone().or_else(|| {
744                        if let ObjKind::DomElement(idx) = cur.borrow().kind {
745                            self.get_dom_node_proto(idx)
746                        } else {
747                            None
748                        }
749                    });
750                    match next {
751                        Some(p) => cur = p,
752                        None => break,
753                    }
754                }
755                // `Object.create(null)`/`Object.groupBy()`等、明示的にnull
756                // プロトタイプ化されたオブジェクト(`null_proto`)は、
757                // `hasOwnProperty`/`toString`等の共通メソッドフォールバックも
758                // 一切持たないのが仕様(`proto: None`が「単に通常オブジェクト
759                // でチェーンが尽きた」のか「明示的にnull」なのかを区別する
760                // ためのフラグ。2026-07-18 発見・実装)。
761                if o.borrow().null_proto {
762                    return Ok(Value::Undefined);
763                }
764                // `x.constructor`(自前・原型に無い): 組込みの prototype の constructor。
765                // 【2026-10-20】`{}.constructor` が undefined だった。YouTube の Polymer(polySi)は
766                // 振る舞いオブジェクトの `.constructor` を `Reflect.construct`/`apply` に使う。
767                if key == "constructor" {
768                    let name = {
769                        let b = o.borrow();
770                        match &b.kind {
771                            ObjKind::Array(_) => "Array",
772                            ObjKind::PromiseObj(_) => "Promise",
773                            ObjKind::MapObj(_) => "Map",
774                            ObjKind::SetObj(_) => "Set",
775                            ObjKind::RegExpObj(_) => "RegExp",
776                            ObjKind::DateObj(_) => "Date",
777                            _ if b.is_callable() => "Function",
778                            _ => "Object",
779                        }
780                    };
781                    if let Some(c) = super::scope_get(&self.global, name) {
782                        return Ok(c);
783                    }
784                }
785                // 組込みメソッド(Array/Object 共通)。
786                Ok(super::super::builtins::object_get(o, key))
787            }
788        }
789    }
790
791    pub fn set_property(&mut self, recv: &Value, key: &str, val: Value) {
792        if let Value::Object(o) = recv {
793            // `Object.freeze()` 済みなら黙殺(非 strict モード相当。以前は freeze 自体が
794            // no-op で、このチェックが無かった)。
795            if o.borrow().frozen {
796                return;
797            }
798            // Proxy: set トラップがあれば呼ぶ。無ければ target にフォワード。
799            let proxy = match &o.borrow().kind {
800                ObjKind::Proxy { target, handler } => Some((target.clone(), handler.clone())),
801                _ => None,
802            };
803            if let Some((target, handler)) = proxy {
804                let trap = handler.borrow().props.get("set").cloned();
805                if let Some(trap) =
806                    trap.filter(|t| matches!(t, Value::Object(f) if f.borrow().is_callable()))
807                {
808                    let args = [Value::Object(target), Value::str(key), val, recv.clone()];
809                    // `set_property` は `()` を返す設計なので例外を上へ返せない。
810                    // 捨てずに報告する(トラップ内の例外が消えると、代入が
811                    // 効かない理由が誰にも分からなくなる)。
812                    let ctx = alloc::format!("Proxy set trap for '{}'", key);
813                    self.call_listener(&trap, Value::Object(handler), &args, &ctx);
814                    return;
815                }
816                self.set_property(&Value::Object(target), key, val);
817                return;
818            }
819            // `window.x = v` は同じ名前のグローバル変数も書き換える(同じ束縛)。
820            if self.is_window_obj(o) {
821                let mut g = self.global.borrow_mut();
822                if let Some(slot) = g.vars.get_mut(key) {
823                    *slot = val.clone();
824                }
825            }
826            // `obj.__proto__ = proto`(Annex B のレガシーアクセサ。`get_property`
827            // 側の同名の欠落と対になるバグ)。`Object.setPrototypeOf` と同じ意味論
828            // (`null`/非オブジェクトならプロトタイプ連鎖を切る)。ただし
829            // `{['__proto__']: v}`(算出キー)で作られた通常の own データ
830            // プロパティが既にある場合は `get_property` と同様そちらを優先し、
831            // 普通のプロパティ代入として扱う。
832            if key == "__proto__" && !o.borrow().props.contains_key("__proto__") {
833                // DOM ノードのハンドルは作り直されるので、原型はノード側に持たせる。
834                let dom_idx = match o.borrow().kind {
835                    ObjKind::DomElement(i) => Some(i),
836                    _ => None,
837                };
838                if let (Some(i), Value::Object(p)) = (dom_idx, &val) {
839                    self.dom.borrow_mut().node_protos.insert(i, p.clone());
840                    return;
841                }
842                match val {
843                    Value::Object(p) => o.borrow_mut().proto = Some(p),
844                    _ => o.borrow_mut().proto = None,
845                }
846                return;
847            }
848            // RadioNodeList.prototype.value = val(HTML Standard §4.10.21.1 setter)。
849            if key == "value" && o.borrow().props.get("_is_radio_nodelist").map(|v| v.truthy()).unwrap_or(false) {
850                let target_val = val.to_js_string();
851                if let ObjKind::Array(ref items) = o.borrow().kind {
852                    let mut dom = self.dom.borrow_mut();
853                    for item in items {
854                        if let Value::Object(item_obj) = item {
855                            if let ObjKind::DomElement(idx) = item_obj.borrow().kind {
856                                let is_radio = dom.nodes.get(idx).map(|n| n.tag == "input").unwrap_or(false)
857                                    && dom.get_attr(idx, "type").as_deref() == Some("radio");
858                                if is_radio {
859                                    let elem_val = dom.get_attr(idx, "value").unwrap_or_else(|| String::from("on"));
860                                    if elem_val == target_val {
861                                        dom.set_attr(idx, "checked", "");
862                                    } else {
863                                        dom.remove_attr(idx, "checked");
864                                    }
865                                }
866                            }
867                        }
868                    }
869                }
870                return;
871            }
872            if key == "selectedIndex" {
873                if let Some(Value::Number(select_idx)) = o.borrow().props.get("_select_idx") {
874                    let idx = *select_idx as usize;
875                    let n = val.to_number() as i32;
876                    self.dom.borrow_mut().set_select_selected_index(idx, n);
877                    return;
878                }
879            }
880            // DOM プロキシへの代入。
881            let dom_kind = {
882                match &o.borrow().kind {
883                    ObjKind::DomElement(i) => Some(DomDisp::Element(*i)),
884                    ObjKind::Host(t)
885                        if t.starts_with("style:")
886                            || t.starts_with("dataset:")
887                            || t.starts_with("classList:") =>
888                    {
889                        Some(DomDisp::Host(t.clone()))
890                    }
891                    _ => None,
892                }
893            };
894            if let Some(d) = dom_kind {
895                self.dom_set_property(d, key, val);
896                // `innerHTML` 等で入ったカスタム要素を、代入の直後に昇格させる。
897                self.process_ce_reactions();
898                return;
899            }
900            // `document.title = "..."` への代入が完全に無視され、常に空文字列固定
901            // だったバグ(`<title>` 要素と一切連動していなかった)。既存の `<title>`
902            // 要素があればそのテキストを更新し、`<head>` はあるが `<title>` が無ければ
903            // 子として新規作成する(仕様は先頭子として挿入だが、この処理系の
904            // `append_child` を使う末尾追加の簡略実装。表示上の見た目には影響しない)。
905            // `<head>` 自体が無い場合(`JsRuntime` 生成直後は DOM が完全に空)は内部
906            // プロパティ `_title` に保存し、getter 側のフォールバックで読めるようにする。
907            let is_document = matches!(&o.borrow().kind, ObjKind::Host(t) if t == "document");
908            if is_document && key == "title" {
909                let s = val.to_js_string();
910                let title_idx = self.dom.borrow().query("title");
911                match title_idx {
912                    Some(i) => self.dom.borrow_mut().set_text_content(i, &s),
913                    None => {
914                        let head_idx = self.dom.borrow().query("head");
915                        match head_idx {
916                            Some(head_idx) => {
917                                let new_title = self.dom.borrow_mut().create_element("title");
918                                self.dom.borrow_mut().set_text_content(new_title, &s);
919                                self.dom.borrow_mut().append_child(head_idx, new_title);
920                            }
921                            None => {
922                                o.borrow_mut().props.insert("_title".into(), Value::str(s.clone()));
923                            }
924                        }
925                    }
926                }
927                return;
928            }
929            // `document.cookie = "name=value; ..."` への代入(丸ごと未対応
930            // だった)。詳細は `builtins::cookie_set` 参照。
931            if is_document && key == "cookie" {
932                super::super::builtins::cookie_set(&val.to_js_string());
933                return;
934            }
935            // localStorage / sessionStorage への代入。
936            let storage_tag = {
937                match &o.borrow().kind {
938                    ObjKind::Host(t) if t.starts_with("storage:") => Some(t.clone()),
939                    _ => None,
940                }
941            };
942            if let Some(t) = storage_tag {
943                super::super::builtins::storage_set_prop(&t, key, &val);
944                return;
945            }
946            // レガシー DOM Level 0 の `event.returnValue`/`event.cancelBubble` への
947            // 代入が丸ごと無視され、単なるプロパティ上書き(何の効果も持たない)に
948            // なっていたバグ。仕様上これらは `preventDefault()`/`stopPropagation()`
949            // と等価の副作用を持つべき。`preventDefault` プロパティ(callable)の
950            // 有無で Event オブジェクトかどうかを判定する(このエンジンの
951            // event_ctor/custom_event_ctor/DOM 要素側いずれも必ず持つ一意な目印)。
952            if key == "returnValue" || key == "cancelBubble" {
953                let is_event = matches!(
954                    o.borrow().props.get("preventDefault"),
955                    Some(Value::Object(f)) if f.borrow().is_callable()
956                );
957                if is_event {
958                    if key == "returnValue" && !val.truthy() {
959                        // `o.borrow()` の一時 Ref を `if let` の条件式に直接書くと
960                        // ブロック終端までライフタイムが延び、`call_value` が呼ぶ
961                        // `preventDefault` 内の `borrow_mut()` と衝突して
962                        // 「RefCell already borrowed」パニックになる(実機/QEMU
963                        // 起動時の自己テストで発覚)。先に所有値へ取り出してから
964                        // 呼び出すことで借用を確実に解放する。
965                        let pd = o.borrow().props.get("preventDefault").cloned();
966                        if let Some(f) = pd {
967                            self.call_listener(&f, recv.clone(), &[], "preventDefault()");
968                        }
969                        return;
970                    }
971                    if key == "cancelBubble" && val.truthy() {
972                        let sp = o.borrow().props.get("stopPropagation").cloned();
973                        if let Some(f) = sp {
974                            self.call_listener(&f, recv.clone(), &[], "stopPropagation()");
975                        }
976                        return;
977                    }
978                    // `returnValue = true` や `cancelBubble = false` は一度止めた
979                    // ものを元に戻す効果を持たない no-op だが、代入値自体は保持する。
980                    o.borrow_mut().props.insert(key.to_string(), val);
981                    return;
982                }
983            }
984            // RegExp.lastIndex への代入(`g`/`y` フラグと組み合わせた手動シーク用)。
985            // 特別扱いしないと汎用 props への書込みで終わり、実際のマッチ開始位置
986            // (RegExpData.last_index)には反映されず sticky/global の再開位置制御が効かない。
987            if key == "lastIndex" {
988                if let ObjKind::RegExpObj(r) = &o.borrow().kind {
989                    let n = val.to_number();
990                    if n.is_finite() && n >= 0.0 {
991                        r.borrow_mut().last_index = n as usize;
992                    }
993                    return;
994                }
995            }
996            // アクセサ(setter)確認。`get_property` と同じ理由で、プロトタイプ連鎖
997            // (class の `set x(v){}` は prototype 上にある)を this を固定したまま
998            // ループで辿る(インスタンス自身だけしか見ていなかった旧実装は、継承した
999            // setter が一切呼ばれず通常のプロパティ書込みにフォールバックしてしまう
1000            // バグだった)。
1001            let mut cur = o.clone();
1002            // `get_property` と同じ理由(`Object.setPrototypeOf` が循環プロトタイプ
1003            // 連鎖を検証しない)で上限ガードが必要。
1004            let mut guard = 0u32;
1005            loop {
1006                let has_accessor_entry = cur.borrow().accessors.contains_key(key);
1007                if has_accessor_entry {
1008                    let setter = cur.borrow().accessors.get(key).and_then(|a| a.set.clone());
1009                    if let Some(s) = setter {
1010                        let ctx = alloc::format!("setter for '{}'", key);
1011                        self.call_listener(&s, recv.clone(), &[val], &ctx);
1012                    }
1013                    // setter が無い(getter のみ)場合も、アクセサ定義がある以上は
1014                    // 通常のデータプロパティ書込みへフォールバックしない(仕様通り)。
1015                    return;
1016                }
1017                guard += 1;
1018                if guard > 1000 {
1019                    break;
1020                }
1021                let next = cur.borrow().proto.clone();
1022                match next {
1023                    Some(p) => cur = p,
1024                    None => break,
1025                }
1026            }
1027            let mut b = o.borrow_mut();
1028            if b.props.contains_key(key) {
1029                if let Some(attr) = b.attrs.get(key) {
1030                    if !attr.writable {
1031                        return;
1032                    }
1033                }
1034            }
1035            // TypedArray(`_ta_kind` タグ付きの `ObjKind::Array` 流用実装)の索引代入
1036            // `ta[i] = v` は、コンストラクタ/`set`/`fill` 経由と違って型変換
1037            // (ToInt8/ToUint8Clamp/... 相当。範囲外の値のラップ/クランプ)が一切
1038            // 効かず生の数値がそのまま入るバグだった(例: `new Int8Array(1); a[0]=200;`
1039            // が `200` のまま、本来は `-56` にラップされるべき)。
1040            let ta_kind_tag = b.props.get("_ta_kind").map(|v| v.to_js_string());
1041            if let ObjKind::Bytes(st) = &b.kind {
1042                if let Ok(idx) = key.parse::<usize>() {
1043                    st.set(idx, val.to_number());
1044                    return;
1045                }
1046                if key == "length" {
1047                    return;
1048                }
1049            }
1050            if let ObjKind::Array(items) = &mut b.kind {
1051                if key == "length" {
1052                    let n = val.to_number();
1053                    if n.is_finite() && n >= 0.0 {
1054                        items.resize(n as usize, Value::Undefined);
1055                    }
1056                    return;
1057                }
1058                if let Ok(idx) = key.parse::<usize>() {
1059                    if idx >= items.len() {
1060                        items.resize(idx + 1, Value::Undefined);
1061                    }
1062                    let val = match &ta_kind_tag {
1063                        Some(tag) => Value::Number(super::super::builtins::ta_coerce_index_value(tag, &val)),
1064                        None => val,
1065                    };
1066                    items[idx] = val;
1067                    // TypedArray のビュー(`subarray` 等)なら元配列へ書き通す。
1068                    if ta_kind_tag.is_some() && b.props.contains_key("_view_root") {
1069                        drop(b);
1070                        super::super::builtins::ta_write_through(o, idx, idx + 1);
1071                    }
1072                    return;
1073                }
1074            }
1075            // `Object.seal()`/`Object.preventExtensions()` 済みなら新規プロパティの追加を
1076            // 拒否する(既存キーの値変更は許可。配列の数値インデックス拡張は対象外の簡略化)。
1077            if (b.sealed || b.non_extensible) && !b.props.contains_key(key) {
1078                return;
1079            }
1080            // location オブジェクトのナビゲーション系プロパティへの代入。
1081            // 例: location.href = "..." / location.hash = "#x" / location.pathname = "/p"。
1082            // ホストが take_pending_location() で回収し実ナビゲーションするよう要求を積む。
1083            // それ以外(origin 等の読取専用や href の文字列保持)は通常通り props に格納する。
1084            let is_location = b.props.contains_key("_is_location");
1085            if is_location {
1086                let new_val = val.to_js_string();
1087                // 【診断】JS によるページ遷移の要求(どの関数から何を代入したか)。
1088                if matches!(key, "href" | "hash" | "pathname" | "search" | "protocol" | "host" | "hostname" | "port") {
1089                    let top: Vec<&str> = self.call_names.iter().rev().take(8).map(|s| s.as_str()).collect();
1090                    crate::warn!("[JSNAV] location.{} = {:?} top={:?}", key, new_val.chars().take(120).collect::<String>(), top);
1091                }
1092                match key {
1093                    "href" => {
1094                        b.props
1095                            .insert(String::from("_pending_location"), Value::str(&new_val));
1096                        b.props
1097                            .insert(String::from("_pending_location_mode"), Value::str("assign"));
1098                    }
1099                    "hash" => {
1100                        // 現在の href からフラグメントだけ差し替えた完全URLを要求に積む。
1101                        let cur = b
1102                            .props
1103                            .get("href")
1104                            .map(|v| v.to_js_string())
1105                            .unwrap_or_default();
1106                        let base = match cur.split_once('#') {
1107                            Some((b, _)) => String::from(b),
1108                            None => cur,
1109                        };
1110                        let frag = new_val.trim_start_matches('#');
1111                        let full = alloc::format!("{}#{}", base, frag);
1112                        b.props
1113                            .insert(String::from("_pending_location"), Value::str(&full));
1114                        b.props
1115                            .insert(String::from("_pending_location_mode"), Value::str("hash"));
1116                    }
1117                    "pathname" | "search" => {
1118                        // protocol+host を保ったまま path / query を差し替える。
1119                        let proto = b
1120                            .props
1121                            .get("protocol")
1122                            .map(|v| v.to_js_string())
1123                            .unwrap_or_else(|| String::from("https:"));
1124                        let host = b
1125                            .props
1126                            .get("host")
1127                            .map(|v| v.to_js_string())
1128                            .unwrap_or_default();
1129                        let pathname = if key == "pathname" {
1130                            new_val.clone()
1131                        } else {
1132                            b.props
1133                                .get("pathname")
1134                                .map(|v| v.to_js_string())
1135                                .unwrap_or_else(|| String::from("/"))
1136                        };
1137                        let search = if key == "search" {
1138                            if new_val.is_empty() || new_val.starts_with('?') {
1139                                new_val.clone()
1140                            } else {
1141                                alloc::format!("?{}", new_val)
1142                            }
1143                        } else {
1144                            b.props
1145                                .get("search")
1146                                .map(|v| v.to_js_string())
1147                                .unwrap_or_default()
1148                        };
1149                        let full = alloc::format!("{}//{}{}{}", proto, host, pathname, search);
1150                        b.props
1151                            .insert(String::from("_pending_location"), Value::str(&full));
1152                        b.props
1153                            .insert(String::from("_pending_location_mode"), Value::str("assign"));
1154                    }
1155                    // `location.protocol`/`.host`/`.hostname`/`.port`への代入も実際に
1156                    // 実 URL では再ナビゲーションを発生させる仕様だが、`href`/`hash`/
1157                    // `pathname`/`search`だけがこの`match`で処理されており、この4つは
1158                    // 常に`_ => {}`(後段で見かけ上の`.props`値は更新されるが、
1159                    // `_pending_location`が積まれずナビゲーション要求にならない)に
1160                    // 落ちていた。`pathname`/`search`分岐と同じ「他のコンポーネントは
1161                    // 現在値を保ったまま、変更対象だけ差し替えてhrefを再構成する」
1162                    // パターンをそのまま適用する。
1163                    "protocol" | "host" | "hostname" | "port" => {
1164                        let proto = if key == "protocol" {
1165                            if new_val.ends_with(':') { new_val.clone() } else { alloc::format!("{}:", new_val) }
1166                        } else {
1167                            b.props.get("protocol").map(|v| v.to_js_string()).unwrap_or_else(|| String::from("https:"))
1168                        };
1169                        let host = if key == "host" {
1170                            // `host`は`hostname`+`:`+`port`の合成値のため、`hostname`/
1171                            // `port`個別プロパティもここで分割して同期しておく(しないと
1172                            // `location.host='a.com:1'`の直後に`location.hostname`/
1173                            // `.port`を読んだ時だけ古い値のままという食い違いが起きる)。
1174                            let (hn, pt) = match new_val.split_once(':') {
1175                                Some((h, p)) => (String::from(h), String::from(p)),
1176                                None => (new_val.clone(), String::new()),
1177                            };
1178                            b.props.insert(String::from("hostname"), Value::str(&hn));
1179                            b.props.insert(String::from("port"), Value::str(&pt));
1180                            new_val.clone()
1181                        } else if key == "hostname" || key == "port" {
1182                            let hostname = if key == "hostname" {
1183                                new_val.clone()
1184                            } else {
1185                                b.props.get("hostname").map(|v| v.to_js_string()).unwrap_or_default()
1186                            };
1187                            let port = if key == "port" {
1188                                new_val.clone()
1189                            } else {
1190                                b.props.get("port").map(|v| v.to_js_string()).unwrap_or_default()
1191                            };
1192                            if port.is_empty() { hostname } else { alloc::format!("{}:{}", hostname, port) }
1193                        } else {
1194                            b.props.get("host").map(|v| v.to_js_string()).unwrap_or_default()
1195                        };
1196                        let pathname = b
1197                            .props
1198                            .get("pathname")
1199                            .map(|v| v.to_js_string())
1200                            .unwrap_or_else(|| String::from("/"));
1201                        let search = b.props.get("search").map(|v| v.to_js_string()).unwrap_or_default();
1202                        let full = alloc::format!("{}//{}{}{}", proto, host, pathname, search);
1203                        b.props
1204                            .insert(String::from("_pending_location"), Value::str(&full));
1205                        b.props
1206                            .insert(String::from("_pending_location_mode"), Value::str("assign"));
1207                    }
1208                    _ => {}
1209                }
1210                // href 等の見かけ上の値も更新しておく(同期読取のため)。
1211                b.props.insert(String::from(key), val);
1212                return;
1213            }
1214            b.props.insert(String::from(key), val);
1215        }
1216    }
1217
1218    // ============ DOM プロキシのプロパティ ============
1219
1220}
1221
1222/// 式を短い文字列にする(エラーの発生箇所の表示用。識別子・メンバー・添字・呼び出しのみ)。
1223pub(crate) fn describe_expr(e: &crate::os_lib::js::ast::Expression) -> String {
1224    use crate::os_lib::js::ast::Expression as E;
1225    let s = match e {
1226        E::Identifier(n) => n.clone(),
1227        E::This => String::from("this"),
1228        E::Member { object, property, .. } => alloc::format!("{}.{}", describe_expr(object), property),
1229        E::Index { object, .. } => alloc::format!("{}[…]", describe_expr(object)),
1230        E::Call { callee, .. } => alloc::format!("{}()", describe_expr(callee)),
1231        // 複合式も中身が分かるように(ミニファイされたコードの `(a=b.c)==null?…` 等)。
1232        E::Assign { op, target, value } => alloc::format!("({}{}{})", describe_expr(target), op, describe_expr(value)),
1233        E::Logical { left, right, .. } => alloc::format!("({}||&&{})", describe_expr(left), describe_expr(right)),
1234        E::Conditional { consequent, alternate, .. } => alloc::format!("(…?{}:{})", describe_expr(consequent), describe_expr(alternate)),
1235        E::Sequence(v) => match v.last() {
1236            Some(l) => alloc::format!("(…,{})", describe_expr(l)),
1237            None => String::from("…"),
1238        },
1239        _ => String::from("…"),
1240    };
1241    if s.chars().count() > 80 {
1242        let t: String = s.chars().rev().take(80).collect::<Vec<_>>().into_iter().rev().collect();
1243        alloc::format!("…{t}")
1244    } else {
1245        s
1246    }
1247}