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