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