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