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}