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}