Skip to main content

atmos/os_lib/js/
interp.rs

1//! ツリーウォーク評価器。
2//!
3//! - スコープは `Rc<RefCell<Scope>>` のチェーン(関数呼出ごとに新スコープ。Phase 1 では
4//!   ブロックスコープは作らず関数スコープ共有)。
5//! - 例外は `Result<_, Value>`(`Err` が throw された値)で伝播。
6//! - 制御フローは `Completion`。
7//! - **ステップ予算**と**再帰深度上限**で無限ループ/暴走から OS を守る。超過時は
8//!   `aborted` を立て、catch では捕捉せず最上位まで巻き戻す。
9
10use alloc::boxed::Box;
11use alloc::collections::{BTreeMap, VecDeque};
12use alloc::format;
13use alloc::rc::Rc;
14use alloc::string::String;
15use alloc::string::ToString;
16use alloc::vec::Vec;
17use core::cell::RefCell;
18
19use super::ast::*;
20use super::dom_bridge::DomBridge;
21use super::lexer::Lexer;
22use super::parser::Parser;
23use super::value::*;
24
25const DEFAULT_MAX_STEPS: u64 = 5_000_000;
26const DEFAULT_MAX_DEPTH: u32 = 400;
27
28/// モジュール本体の評価スコープで export 収集オブジェクトを束縛する番兵キー。
29/// 通常の識別子と衝突しない制御文字を含める。
30const MODULE_EXPORTS_KEY: &str = "\u{0}module.exports";
31
32/// 登録済み ES モジュール 1 件。`exports` が None なら未評価。
33/// 評価開始時に空の共有マップを先に入れておくことで循環 import に耐える
34/// (再入時は評価途中の部分マップが返る)。
35pub struct ModuleRecord {
36    pub source: String,
37    pub exports: Option<Rc<RefCell<BTreeMap<String, Value>>>>,
38}
39
40/// 指定子 → モジュール記録のレジストリ(ランタイムと実行器で共有)。
41pub type ModuleRegistry = Rc<RefCell<BTreeMap<String, ModuleRecord>>>;
42
43/// 変数スコープ。
44pub struct Scope {
45    pub vars: BTreeMap<String, Value>,
46    pub parent: Option<Rc<RefCell<Scope>>>,
47}
48
49impl Scope {
50    pub fn new_root() -> Rc<RefCell<Scope>> {
51        Rc::new(RefCell::new(Scope {
52            vars: BTreeMap::new(),
53            parent: None,
54        }))
55    }
56    pub fn child(parent: Rc<RefCell<Scope>>) -> Rc<RefCell<Scope>> {
57        Rc::new(RefCell::new(Scope {
58            vars: BTreeMap::new(),
59            parent: Some(parent),
60        }))
61    }
62}
63
64pub(crate) fn scope_get(scope: &Rc<RefCell<Scope>>, name: &str) -> Option<Value> {
65    let b = scope.borrow();
66    if let Some(v) = b.vars.get(name) {
67        return Some(v.clone());
68    }
69    match &b.parent {
70        Some(p) => scope_get(p, name),
71        None => None,
72    }
73}
74
75/// 既存変数へ代入(最も近い定義スコープ)。未定義なら false。
76fn scope_assign(scope: &Rc<RefCell<Scope>>, name: &str, val: Value) -> bool {
77    {
78        let mut b = scope.borrow_mut();
79        if b.vars.contains_key(name) {
80            b.vars.insert(name.to_string(), val);
81            return true;
82        }
83    }
84    let parent = scope.borrow().parent.clone();
85    match parent {
86        Some(p) => scope_assign(&p, name, val),
87        None => false,
88    }
89}
90
91fn scope_declare(scope: &Rc<RefCell<Scope>>, name: &str, val: Value) {
92    scope.borrow_mut().vars.insert(name.to_string(), val);
93}
94
95/// NamedEvaluation(ES2015 13.15.2 等)の簡易実装: `v` が無名関数(`fd.name` が空)
96/// なら `name` を継承させる。`const f = function(){}` 等、単純な識別子への代入経路
97/// でのみ対応(object literal のプロパティ/デフォルト引数値としての NamedEvaluation は
98/// 非対応の簡略化)。既に名前を持つ関数(`function foo(){}` 等)は上書きしない。
99fn infer_function_name(v: &Value, name: &str) {
100    if let Value::Object(o) = v {
101        if let ObjKind::Function(fd) = &mut o.borrow_mut().kind {
102            if fd.name.is_empty() {
103                fd.name = String::from(name);
104            }
105        }
106    }
107}
108
109/// 文の完了種別。`Break`/`Continue` の `Option<String>` はラベル(`break label;` 等)。
110pub(crate) enum Completion {
111    Normal(Value),
112    Return(Value),
113    Break(Option<String>),
114    Continue(Option<String>),
115}
116
117/// generator 本体の replay 実行中に保持する一時状態。
118///
119/// resume のたびに本体を先頭から再実行し、`target` 番目の yield で中断(suspend)する。
120/// それより手前の yield には `sent` に保存済みの resume 値を返して通過させる。
121struct GenReplay {
122    /// 既に通過した各 yield の resume 結果。
123    sent: Vec<super::value::GenCompletion>,
124    /// この replay でこれまでに遭遇した yield 数。
125    counter: usize,
126    /// この index の yield に達したら中断する(= sent.len())。
127    target: usize,
128    /// 中断(suspend)中フラグ。Err を最上位まで巻き戻すための番兵。
129    suspending: bool,
130    /// 中断地点で yield された値。
131    yielded: Value,
132    /// 強制Return時の戻り値を一時保持する。
133    returning: Option<Value>,
134}
135
136/// ページ単位で永続するランタイム状態。
137pub struct JsRuntime {
138    pub global: Rc<RefCell<Scope>>,
139    /// console.* の累積出力(ページ表示やテスト用)。
140    pub out: String,
141    /// JS ⇄ レンダラの DOM ブリッジ(ページ寿命を通じて永続。リスナを保持)。
142    pub dom: Rc<RefCell<DomBridge>>,
143    /// Promise の then 反応を遅延実行するマイクロタスクキュー。
144    pub microtasks: Rc<RefCell<VecDeque<Job>>>,
145    /// setTimeout のコールバック(マクロタスク: callback, args, id)。
146    pub macrotasks: Rc<RefCell<VecDeque<(Value, Vec<Value>, u64)>>>,
147    /// 現在ページの絶対URL(fetch/XHR の相対URL解決の基準。未設定なら空)。
148    pub base_url: String,
149    /// 登録済み ES モジュール(指定子 → ソース/評価済みエクスポート)。
150    pub modules: ModuleRegistry,
151    /// 直近の `eval()` で検出した構文エラー。
152    ///
153    /// 【2026-07-28】パーサはベストエフォートで回復して継続する設計だが、
154    /// 従来は回復した事実を誰にも伝えていなかったため、**壊れたスクリプトが
155    /// 正常に動いていないのか、そもそも解析に失敗していたのかを区別できなかった**。
156    /// 実行は従来どおり継続しつつ、呼び出し側が参照できるようここへ残す。
157    pub last_syntax_errors: Vec<super::parser::ParseError>,
158}
159
160impl Default for JsRuntime {
161    fn default() -> Self {
162        Self::new()
163    }
164}
165
166impl JsRuntime {
167    pub fn new() -> Self {
168        let global = Scope::new_root();
169        super::builtins::install(&global);
170        JsRuntime {
171            global,
172            out: String::new(),
173            dom: Rc::new(RefCell::new(DomBridge::new())),
174            microtasks: Rc::new(RefCell::new(VecDeque::new())),
175            macrotasks: Rc::new(RefCell::new(VecDeque::new())),
176            base_url: String::new(),
177            modules: Rc::new(RefCell::new(BTreeMap::new())),
178            last_syntax_errors: Vec::new(),
179        }
180    }
181
182    fn new_interp(&self) -> Interp {
183        // base_url の真実源は共有 location オブジェクト(pushState 等での変更も反映)。
184        // 未設定なら runtime の base_url にフォールバック。
185        let base_url = super::builtins::location_href(&self.global)
186            .filter(|s| !s.is_empty())
187            .unwrap_or_else(|| self.base_url.clone());
188        Interp {
189            steps: 0,
190            max_steps: DEFAULT_MAX_STEPS,
191            depth: 0,
192            max_depth: DEFAULT_MAX_DEPTH,
193            aborted: false,
194            out: String::new(),
195            dom: self.dom.clone(),
196            microtasks: self.microtasks.clone(),
197            gen_replay: None,
198            macrotasks: self.macrotasks.clone(),
199            base_url,
200            global: self.global.clone(),
201            io_callbacks: Vec::new(),
202            intervals: Vec::new(),
203            modules: self.modules.clone(),
204            pending_label: None,
205            pending_new_target: None,
206            new_target_stack: Vec::new(),
207        }
208    }
209
210    /// 現在ページの絶対URLを設定。fetch/XHR の相対URL基準(base_url)と
211    /// JS の window/document.location オブジェクトを同時に更新する。
212    pub fn set_page_url(&mut self, url: &str) {
213        self.base_url = String::from(url);
214        super::builtins::update_location(&self.global, url);
215    }
216
217    /// ES モジュールをソース付きで登録する(指定子 → ソース)。
218    /// import 時に遅延評価される。同じ指定子の再登録は上書き(未評価状態に戻す)。
219    pub fn define_module(&mut self, specifier: &str, source: &str) {
220        self.modules.borrow_mut().insert(
221            String::from(specifier),
222            ModuleRecord {
223                source: String::from(source),
224                exports: None,
225            },
226        );
227    }
228
229    /// ソースを評価。戻り値は最後の式の値、または throw された値の文字列化。
230    ///
231    /// 構文エラーがあっても(従来どおり)ベストエフォートで実行を続けるが、
232    /// **検出した構文エラーは必ずログへ出し、`last_syntax_errors` に残す**。
233    /// 「スクリプトが動かない」ときに、解析で諦めた箇所が分かるようにするため。
234    pub fn eval(&mut self, source: &str) -> Result<Value, String> {
235        let mut parser = Parser::new(Lexer::new(source));
236        let program = parser.parse_program();
237        let total_errors = parser.error_count();
238        self.last_syntax_errors = parser.take_errors();
239        if total_errors > 0 {
240            crate::warn!(
241                "[JS] {} syntax error(s) recovered; executing best-effort",
242                total_errors
243            );
244            for e in self.last_syntax_errors.iter().take(5) {
245                crate::warn!(
246                    "[JS] SyntaxError: {} (token #{})",
247                    e.message,
248                    e.token_index
249                );
250            }
251        }
252        let mut interp = self.new_interp();
253        let scope = self.global.clone();
254        let result = interp.exec_statements(&program.body, &scope, &Value::Undefined);
255        // 保留中の Promise 反応・setTimeout を消化する。
256        interp.run_event_loop();
257        // IntersectionObserver コールバックを全要素 visible で発火(ページ初期化の最後に)。
258        super::builtins::fire_intersection_observers(&mut interp);
259        // MutationObserver コールバック発火(eval 中の DOM 変更を通知)。
260        super::builtins::flush_mutation_observers(&mut interp);
261        // 出力を取り込む。
262        self.out.push_str(&interp.out);
263        if interp.aborted {
264            return Err(String::from("script aborted (step/depth budget exceeded)"));
265        }
266        match result {
267            Ok(Completion::Normal(v)) | Ok(Completion::Return(v)) => Ok(v),
268            Ok(_) => Ok(Value::Undefined),
269            Err(thrown) => {
270                let err_str = thrown.to_js_string();
271                if err_str == "[object Object]" {
272                    if let Value::Object(o) = &thrown {
273                        let name = o.borrow().props.get("name").map(|v| v.to_js_string()).unwrap_or_else(|| alloc::string::String::from("Error"));
274                        let msg = o.borrow().props.get("message").map(|v| v.to_js_string()).unwrap_or_else(|| alloc::string::String::from(""));
275                        let stack = o.borrow().props.get("stack").map(|v| v.to_js_string()).unwrap_or_else(|| alloc::string::String::from(""));
276                        crate::println!("[JS_ERR] {}: {}\nStack:\n{}", name, msg, stack);
277                    }
278                }
279                Err(err_str)
280            }
281        }
282    }
283
284    /// 指定ノードの click イベントを発火する(バブリング対応)。ホストがクリック処理から呼ぶ。
285    /// DOM 変更があれば `dom.borrow().dirty` が立つので、呼び元が再レイアウトする。
286    pub fn dispatch_click(&mut self, node_idx: usize) -> bool {
287        self.dispatch_event_with(node_idx, "click", &[]).0
288    }
289
290    /// 汎用イベントディスパッチ(追加プロパティ無し)。発火したか返す。
291    pub fn dispatch_event(&mut self, node_idx: usize, event_type: &str) -> bool {
292        // `:user-valid`/`:user-invalid`(CSS Selectors Level 4)が丸ごと未対応
293        // だった。`:indeterminate`の`_indeterminate`と同じ「JS/描画側の内部専用
294        // 属性キーとしてCSSセレクタマッチャーに渡す」パターンで、ユーザーが実際に
295        // フォームコントロールを操作した(`input`/`change`イベントが発火した)
296        // 事実だけを`_user_interacted`属性として記録する(値の正誤判定自体は
297        // 既存の`:valid`/`:invalid`ロジックを再利用するため、ここでは記録のみ)。
298        // 2026-07-18 発見・実装。
299        if matches!(event_type, "input" | "change") {
300            self.dom.borrow_mut().set_attr(node_idx, "_user_interacted", "1");
301        }
302        self.dispatch_event_with(node_idx, event_type, &[]).0
303    }
304
305    /// マウスイベント(click/mouseover/mouseout/mousemove 等)。座標 `clientX`/`clientY`/
306    /// `pageX`/`pageY` と `button` を付与。戻り値: (リスナ発火, preventDefault されたか)。
307    pub fn dispatch_mouse(
308        &mut self,
309        node_idx: usize,
310        event_type: &str,
311        x: i32,
312        y: i32,
313    ) -> (bool, bool) {
314        self.dispatch_mouse_full(node_idx, event_type, x, y, 0, 0)
315    }
316
317    /// マウスイベント完全版。`button`(0=左,1=中,2=右)と `wheel_delta` を指定でき、
318    /// MouseEvent 標準プロパティ(clientX/Y, pageX/Y, screenX/Y, offsetX/Y, movementX/Y,
319    /// button, buttons, detail, 修飾キー shiftKey/ctrlKey/altKey/metaKey)を付与する。
320    /// wheel イベントの場合は deltaX/deltaY/deltaMode も付与する。
321    pub fn dispatch_mouse_full(
322        &mut self,
323        node_idx: usize,
324        event_type: &str,
325        x: i32,
326        y: i32,
327        button: i32,
328        wheel_delta: i32,
329    ) -> (bool, bool) {
330        // buttons ビットマスク: 1=左,2=右,4=中(押下中のみ。click/up 等では 0)。
331        let buttons = match event_type {
332            "mousedown" => match button {
333                0 => 1,
334                2 => 2,
335                1 => 4,
336                _ => 0,
337            },
338            _ => 0,
339        };
340        // detail: クリック回数(click=1, dblclick=2)。
341        let detail = match event_type {
342            "click" | "mousedown" | "mouseup" => 1.0,
343            "dblclick" => 2.0,
344            _ => 0.0,
345        };
346        // 要素ローカル座標(offsetX/Y)。レイアウト box は interp 層から参照できないため、
347        // 現状はクライアント座標と同値(呼び出し側でローカル変換済みの座標が渡る)。
348        let (ox, oy) = (x, y);
349        let mut extra = alloc::vec![
350            (String::from("clientX"), Value::Number(x as f64)),
351            (String::from("clientY"), Value::Number(y as f64)),
352            (String::from("pageX"), Value::Number(x as f64)),
353            (String::from("pageY"), Value::Number(y as f64)),
354            (String::from("screenX"), Value::Number(x as f64)),
355            (String::from("screenY"), Value::Number(y as f64)),
356            (String::from("offsetX"), Value::Number(ox as f64)),
357            (String::from("offsetY"), Value::Number(oy as f64)),
358            (String::from("movementX"), Value::Number(0.0)),
359            (String::from("movementY"), Value::Number(0.0)),
360            (String::from("button"), Value::Number(button as f64)),
361            (String::from("buttons"), Value::Number(buttons as f64)),
362            (String::from("detail"), Value::Number(detail)),
363        ];
364        // wheel イベントには delta を付与(縦スクロールのみ。1 notch = 100px 相当)。
365        if event_type == "wheel" {
366            extra.push((String::from("deltaX"), Value::Number(0.0)));
367            extra.push((
368                String::from("deltaY"),
369                Value::Number((wheel_delta * 100) as f64),
370            ));
371            extra.push((String::from("deltaZ"), Value::Number(0.0)));
372            extra.push((String::from("deltaMode"), Value::Number(0.0))); // 0=pixel
373        }
374        Self::push_modifier_props(&mut extra);
375        self.dispatch_event_with(node_idx, event_type, &extra)
376    }
377
378    /// 現在のグローバル修飾キー状態を shiftKey/ctrlKey/altKey/metaKey として extra へ追加する。
379    fn push_modifier_props(extra: &mut Vec<(String, Value)>) {
380        extra.push((
381            String::from("shiftKey"),
382            Value::Bool(crate::kernel::keyboard::shift_down()),
383        ));
384        extra.push((
385            String::from("ctrlKey"),
386            Value::Bool(crate::kernel::keyboard::ctrl_down()),
387        ));
388        extra.push((
389            String::from("altKey"),
390            Value::Bool(crate::kernel::keyboard::alt_down()),
391        ));
392        extra.push((
393            String::from("metaKey"),
394            Value::Bool(crate::kernel::keyboard::meta_down()),
395        ));
396    }
397
398    /// キーイベント(keydown/keyup/keypress/input 等)。`key`/`keyCode`/`which`/`code`/
399    /// `location`/`repeat` と修飾キーを付与。`key` は呼び出し側で正規化済みの値を渡す
400    /// ("a", "Enter", "ArrowLeft" 等)。戻り値: (リスナ発火, preventDefault されたか)。
401    pub fn dispatch_key(&mut self, node_idx: usize, event_type: &str, key: &str) -> (bool, bool) {
402        self.dispatch_key_full(node_idx, event_type, key, false)
403    }
404
405    /// キーイベント完全版。`repeat`(オートリピート)を指定できる。
406    /// `keyCode`/`which` は legacy 仕様に沿って特殊キーへ既定コードを割り当てる。
407    pub fn dispatch_key_full(
408        &mut self,
409        node_idx: usize,
410        event_type: &str,
411        key: &str,
412        repeat: bool,
413    ) -> (bool, bool) {
414        let (key_code, code, location) = Self::key_attributes(key);
415        let mut extra = alloc::vec![
416            (String::from("key"), Value::str(key)),
417            (String::from("keyCode"), Value::Number(key_code as f64)),
418            (String::from("which"), Value::Number(key_code as f64)),
419            (
420                String::from("charCode"),
421                Value::Number(if event_type == "keypress" {
422                    key_code as f64
423                } else {
424                    0.0
425                })
426            ),
427            (String::from("code"), Value::str(&code)),
428            (String::from("location"), Value::Number(location as f64)),
429            (String::from("repeat"), Value::Bool(repeat)),
430        ];
431        Self::push_modifier_props(&mut extra);
432        self.dispatch_event_with(node_idx, event_type, &extra)
433    }
434
435    /// `key` 値から (keyCode, code, location) を導出する。
436    /// 特殊キーは UI Events 仕様の標準 `code` と legacy `keyCode` に対応づける。
437    fn key_attributes(key: &str) -> (u32, String, u32) {
438        // location: 0=標準, 1=左, 2=右, 3=テンキー。本実装では基本 0。
439        match key {
440            "Enter" => (13, String::from("Enter"), 0),
441            "Tab" => (9, String::from("Tab"), 0),
442            "Backspace" => (8, String::from("Backspace"), 0),
443            "Escape" => (27, String::from("Escape"), 0),
444            " " => (32, String::from("Space"), 0),
445            "Delete" => (46, String::from("Delete"), 0),
446            "ArrowLeft" => (37, String::from("ArrowLeft"), 0),
447            "ArrowUp" => (38, String::from("ArrowUp"), 0),
448            "ArrowRight" => (39, String::from("ArrowRight"), 0),
449            "ArrowDown" => (40, String::from("ArrowDown"), 0),
450            "Home" => (36, String::from("Home"), 0),
451            "End" => (35, String::from("End"), 0),
452            "PageUp" => (33, String::from("PageUp"), 0),
453            "PageDown" => (34, String::from("PageDown"), 0),
454            "Shift" => (16, String::from("ShiftLeft"), 1),
455            "Control" => (17, String::from("ControlLeft"), 1),
456            "Alt" => (18, String::from("AltLeft"), 1),
457            "Meta" => (91, String::from("MetaLeft"), 1),
458            _ => {
459                // 単一文字: 英字は KeyX、数字は DigitN、その他は文字コードのみ。
460                let mut chars = key.chars();
461                if let (Some(c), None) = (chars.next(), chars.clone().next()) {
462                    let upper = c.to_ascii_uppercase();
463                    if upper.is_ascii_alphabetic() {
464                        let code = alloc::format!("Key{}", upper);
465                        return (upper as u32, code, 0);
466                    }
467                    if c.is_ascii_digit() {
468                        let code = alloc::format!("Digit{}", c);
469                        return (c as u32, code, 0);
470                    }
471                    return (c as u32, String::new(), 0);
472                }
473                (0, String::new(), 0)
474            }
475        }
476    }
477
478    /// 汎用イベントディスパッチ本体: capture(祖先→target)→ target → bubble(target→祖先)の
479    /// 3フェーズで各ノードのリスナを発火。Event は type/target/currentTarget/eventPhase/bubbles/
480    /// defaultPrevented + preventDefault/stopPropagation/stopImmediatePropagation/composedPath を
481    /// 持ち、`extra` の各プロパティも付与する。once リスナは発火後に削除する。
482    /// 戻り値: (リスナが発火したか, defaultPrevented か)。
483    pub fn dispatch_event_with(
484        &mut self,
485        node_idx: usize,
486        event_type: &str,
487        extra: &[(String, Value)],
488    ) -> (bool, bool) {
489        // 伝播パス(target→祖先)。
490        let path = {
491            let dom = self.dom.borrow();
492            let mut path = alloc::vec![node_idx];
493            let mut cur = node_idx;
494            let mut guard = 0;
495            while let Some(p) = dom.nodes.get(cur).and_then(|n| n.parent) {
496                path.push(p);
497                cur = p;
498                guard += 1;
499                if guard > dom.nodes.len() {
500                    break;
501                }
502            }
503            path
504        };
505        let any = path
506            .iter()
507            .any(|&n| self.dom.borrow().has_listener_on(n, event_type));
508        if !any {
509            return (false, false);
510        }
511
512        // `bubbles` を `extra` から読む(無ければ内部発火イベント向けの既定値として
513        // `true` を使う)。以前はここを一切見ずバブリング段階を常に無条件で全祖先
514        // まで辿っていたため、`bubbles: false` を指定したイベントでも祖先の
515        // リスナーが発火してしまう仕様違反バグだった(capture 段階は仕様上
516        // `bubbles` に関わらず常に祖先を辿るため、そちらは変更しない)。
517        let bubbles = extra
518            .iter()
519            .find(|(k, _)| k == "bubbles")
520            .map(|(_, v)| v.truthy())
521            .unwrap_or(true);
522        // composedPath() 用の DOM ハンドル列(target→祖先)。
523        let composed: Vec<Value> = path.iter().map(|&n| Value::Object(Obj::dom(n))).collect();
524
525        // 共有 Event オブジェクト(伝播中ずっと同一)。
526        let target = Value::Object(Obj::dom(node_idx));
527        let ev = Obj::plain();
528        {
529            let mut e = ev.borrow_mut();
530            e.props.insert(String::from("type"), Value::str(event_type));
531            e.props.insert(String::from("target"), target.clone());
532            e.props.insert(String::from("bubbles"), Value::Bool(true));
533            // `cancelable` が一切読まれておらず、`{cancelable: false}` で構築した
534            // イベントでも `preventDefault()` が常に効いてしまう仕様違反バグだった。
535            // 内部発火(click/submit 等)は `extra` が空のまま呼ばれ続けており、
536            // それらは「妨害可能」という既存の挙動に依存しているため、`bubbles`
537            // と同じく既定値は `true`(後段の `extra` ループが明示指定時のみ上書き)。
538            e.props.insert(String::from("cancelable"), Value::Bool(true));
539            // `isTrusted` が丸ごと未対応だった。仕様上、スクリプトから発火した
540            // イベント(`dispatchEvent()`・内部シミュレートの `.click()` 等)は
541            // 常に `false`(実ユーザー入力由来のイベントのみ `true`。この処理系に
542            // その区別自体が無いため常に `false` で問題ない)。読み取り専用の
543            // 定数のため `extra` での上書きは考慮しない。
544            e.props.insert(String::from("isTrusted"), Value::Bool(false));
545            e.props
546                .insert(String::from("eventPhase"), Value::Number(0.0));
547            e.props
548                .insert(String::from("defaultPrevented"), Value::Bool(false));
549            // `timeStamp` が丸ごと未対応で常に `undefined` だった(このエンジン
550            // 全体に存在しない機能だった。`EventTarget` 経路にも同時に追加済み)。
551            e.props.insert(
552                String::from("timeStamp"),
553                Value::Number(super::builtins::next_perf_timestamp()),
554            );
555            // レガシー DOM Level 0 の `returnValue`/`cancelBubble`/`srcElement`
556            // が丸ごと未対応だった。`srcElement` は `target` と同じ値の別名。
557            e.props.insert(String::from("returnValue"), Value::Bool(true));
558            e.props.insert(String::from("cancelBubble"), Value::Bool(false));
559            e.props.insert(String::from("srcElement"), target.clone());
560            e.props.insert(
561                String::from("_composedPath"),
562                Value::Object(Obj::array(composed)),
563            );
564            for (k, v) in extra {
565                e.props.insert(k.clone(), v.clone());
566            }
567            e.props.insert(
568                String::from("preventDefault"),
569                Value::Object(Obj::native("preventDefault", |_, this, _| {
570                    if let Value::Object(o) = &this {
571                        let cancelable = o
572                            .borrow()
573                            .props
574                            .get("cancelable")
575                            .map(|v| v.truthy())
576                            .unwrap_or(true);
577                        if cancelable {
578                            let mut b = o.borrow_mut();
579                            b.props
580                                .insert(String::from("defaultPrevented"), Value::Bool(true));
581                            // レガシー DOM Level 0 の `event.returnValue`
582                            // (`preventDefault()` と等価の意味を持つべきだが
583                            // 丸ごと未対応で常に `undefined` だった)を追従させる。
584                            b.props.insert(String::from("returnValue"), Value::Bool(false));
585                        }
586                    }
587                    Ok(Value::Undefined)
588                })),
589            );
590            e.props.insert(
591                String::from("stopPropagation"),
592                Value::Object(Obj::native("stopPropagation", |_, this, _| {
593                    if let Value::Object(o) = &this {
594                        let mut b = o.borrow_mut();
595                        b.props
596                            .insert(String::from("_stop"), Value::Bool(true));
597                        // レガシー DOM Level 0 の `event.cancelBubble`(同じく
598                        // 丸ごと未対応だった)を追従させる。
599                        b.props.insert(String::from("cancelBubble"), Value::Bool(true));
600                    }
601                    Ok(Value::Undefined)
602                })),
603            );
604            e.props.insert(
605                String::from("stopImmediatePropagation"),
606                Value::Object(Obj::native("stopImmediatePropagation", |_, this, _| {
607                    if let Value::Object(o) = &this {
608                        let mut b = o.borrow_mut();
609                        b.props.insert(String::from("_stop"), Value::Bool(true));
610                        b.props
611                            .insert(String::from("_stopImmediate"), Value::Bool(true));
612                        b.props.insert(String::from("cancelBubble"), Value::Bool(true));
613                    }
614                    Ok(Value::Undefined)
615                })),
616            );
617            e.props.insert(
618                String::from("composedPath"),
619                Value::Object(Obj::native("composedPath", |_, this, _| {
620                    if let Value::Object(o) = &this {
621                        if let Some(p) = o.borrow().props.get("_composedPath") {
622                            return Ok(p.clone());
623                        }
624                    }
625                    Ok(Value::Object(Obj::array(Vec::new())))
626                })),
627            );
628        }
629
630        // フェーズ列を構築: capture(祖先→target、target除く)→ target → bubble(target→祖先、target除く)。
631        // eventPhase: 1=capturing, 2=at_target, 3=bubbling。
632        // 各エントリ: (node, want_capture, phase)。
633        let mut steps: Vec<(usize, bool, f64)> = Vec::new();
634        // capture: path は target→祖先なので逆順(祖先→target)。target 自身は除く。
635        for &n in path.iter().skip(1).rev() {
636            steps.push((n, true, 1.0));
637        }
638        // at target: capture リスナ→bubble リスナの順で両方発火。
639        steps.push((node_idx, true, 2.0));
640        steps.push((node_idx, false, 2.0));
641        // bubble: target→祖先。target 自身は除く。`bubbles: false` なら祖先へ伝播しない。
642        if bubbles {
643            for &n in path.iter().skip(1) {
644                steps.push((n, false, 3.0));
645            }
646        }
647
648        let mut fired = false;
649        let mut to_remove: Vec<u64> = Vec::new();
650        'outer: for (n, want_capture, phase) in steps {
651            let listeners = self
652                .dom
653                .borrow()
654                .listeners_phase(n, event_type, want_capture);
655            if listeners.is_empty() {
656                continue;
657            }
658            {
659                let mut e = ev.borrow_mut();
660                e.props
661                    .insert(String::from("currentTarget"), Value::Object(Obj::dom(n)));
662                e.props
663                    .insert(String::from("eventPhase"), Value::Number(phase));
664            }
665            let node_this = Value::Object(Obj::dom(n));
666            for (func, once, id) in listeners {
667                let mut interp = self.new_interp();
668                let ctx = alloc::format!("{} listener", event_type);
669                interp.call_listener(
670                    &func,
671                    node_this.clone(),
672                    &[Value::Object(ev.clone())],
673                    &ctx,
674                );
675                interp.run_event_loop();
676                self.out.push_str(&interp.out);
677                fired = true;
678                if once {
679                    to_remove.push(id);
680                }
681                if ev
682                    .borrow()
683                    .props
684                    .get("_stopImmediate")
685                    .map(|v| v.truthy())
686                    .unwrap_or(false)
687                {
688                    break 'outer;
689                }
690            }
691            if ev
692                .borrow()
693                .props
694                .get("_stop")
695                .map(|v| v.truthy())
696                .unwrap_or(false)
697            {
698                break 'outer;
699            }
700        }
701        // once リスナを削除。
702        if !to_remove.is_empty() {
703            let mut dom = self.dom.borrow_mut();
704            for id in to_remove {
705                dom.remove_listener_by_id(id);
706            }
707        }
708        let default_prevented = ev
709            .borrow()
710            .props
711            .get("defaultPrevented")
712            .map(|v| v.truthy())
713            .unwrap_or(false);
714        (fired, default_prevented)
715    }
716
717    /// form.reset() が積んだリセット要求を回収する。戻り値: Some(form node idx)。
718    /// ホストが JS 実行後に呼んで実リセットする。
719    pub fn take_pending_reset(&mut self) -> Option<usize> {
720        let g = self.global.clone();
721        let mut gb = g.borrow_mut();
722        let v = match gb.vars.get("__pending_reset") {
723            Some(Value::Number(n)) if *n >= 1.0 => *n as usize - 1,
724            _ => return None,
725        };
726        gb.vars.insert("__pending_reset".into(), Value::Number(0.0));
727        Some(v)
728    }
729
730    /// history.back/forward/go が積んだナビゲーション要求を回収し 0 にリセットする。
731    /// 戻り値: 相対移動量(負=戻る / 正=進む / 0=要求なし)。ホストが JS 実行後に呼ぶ。
732    pub fn take_pending_nav(&mut self) -> i64 {
733        let history = match self.global.borrow().vars.get("history") {
734            Some(Value::Object(h)) => h.clone(),
735            _ => return 0,
736        };
737        let mut hb = history.borrow_mut();
738        let n = match hb.props.get("_pending_nav") {
739            Some(Value::Number(n)) => *n as i64,
740            _ => 0,
741        };
742        if n != 0 {
743            hb.props.insert("_pending_nav".into(), Value::Number(0.0));
744        }
745        n
746    }
747
748    /// location への代入や assign/replace/reload が積んだナビゲーション要求を回収する。
749    /// 戻り値: Some((url, mode)) — mode は "assign"(履歴に積む) / "replace"(置換) /
750    /// "hash"(同一ページ内フラグメント) / "reload"(再読込)。要求が無ければ None。
751    /// ホストが JS 実行後に呼んで実ナビゲーションする。
752    pub fn take_pending_location(&mut self) -> Option<(String, String)> {
753        let location = match self.global.borrow().vars.get("location") {
754            Some(Value::Object(l)) => l.clone(),
755            _ => return None,
756        };
757        let mut lb = location.borrow_mut();
758        let url = match lb.props.get("_pending_location") {
759            Some(v) => v.to_js_string(),
760            _ => return None,
761        };
762        let mode = lb
763            .props
764            .get("_pending_location_mode")
765            .map(|v| v.to_js_string())
766            .unwrap_or_else(|| String::from("assign"));
767        lb.props.shift_remove("_pending_location");
768        lb.props.shift_remove("_pending_location_mode");
769        Some((url, mode))
770    }
771
772    /// window の隠し props 配列(_scroll_listeners / _popstate_listeners 等)に
773    /// 登録されたリスナを複製して返す(無ければ空)。エントリは
774    /// `addEventListener` の `{signal}` 対応(2026-07-14 発見・実装。
775    /// `builtins::push_window_listener` 参照)のため `[cb, signal]` 形式で
776    /// 格納されており、ここで abort 済みのものを除外しつつ `cb` のみへ
777    /// 展開する。
778    fn window_listeners(&self, key: &str) -> Vec<Value> {
779        let window = match self.global.borrow().vars.get("window") {
780            Some(Value::Object(w)) => w.clone(),
781            _ => return Vec::new(),
782        };
783        let entries = match window.borrow().props.get(key) {
784            Some(Value::Object(arr)) => match &arr.borrow().kind {
785                ObjKind::Array(items) => items.clone(),
786                _ => Vec::new(),
787            },
788            _ => Vec::new(),
789        };
790        entries
791            .iter()
792            .filter_map(|entry| {
793                let Value::Object(o) = entry else {
794                    return Some(entry.clone());
795                };
796                let ObjKind::Array(items) = &o.borrow().kind else {
797                    return Some(entry.clone());
798                };
799                if items.get(1).is_some_and(super::dom_bridge::is_signal_aborted) {
800                    return None;
801                }
802                items.first().cloned()
803            })
804            .collect()
805    }
806
807    /// 指定リスナ群を共有イベントオブジェクト(type + extra props)で順に発火する。
808    fn fire_window_listeners(
809        &mut self,
810        listeners: &[Value],
811        event_type: &str,
812        extra: &[(String, Value)],
813    ) {
814        if listeners.is_empty() {
815            return;
816        }
817        let ev = Obj::plain();
818        {
819            let mut e = ev.borrow_mut();
820            e.props.insert(String::from("type"), Value::str(event_type));
821            for (k, v) in extra {
822                e.props.insert(k.clone(), v.clone());
823            }
824        }
825        for cb in listeners {
826            let mut interp = self.new_interp();
827            let ctx = alloc::format!("{} listener (window)", event_type);
828            interp.call_listener(cb, Value::Undefined, &[Value::Object(ev.clone())], &ctx);
829            interp.run_event_loop();
830            self.out.push_str(&interp.out);
831        }
832    }
833
834    /// window に登録された popstate リスナを発火する(history.back/forward/go の実ナビ後にホストが呼ぶ)。
835    /// `state` は history.state を引き継ぐ。
836    pub fn fire_popstate(&mut self) {
837        let listeners = self.window_listeners("_popstate_listeners");
838        // リスナがなくても location.href の変化だけで SPA が動く場合もあるため継続。
839        let state = match self.global.borrow().vars.get("history") {
840            Some(Value::Object(h)) => h
841                .borrow()
842                .props
843                .get("state")
844                .cloned()
845                .unwrap_or(Value::Null),
846            _ => Value::Null,
847        };
848        if !listeners.is_empty() {
849            self.fire_window_listeners(&listeners, "popstate", &[(String::from("state"), state)]);
850        }
851    }
852
853    /// SPA back スタック(pushState で積んだ旧 URL/state)にエントリがあるか。
854    pub fn has_spa_back(&self) -> bool {
855        let history = match self.global.borrow().vars.get("history") {
856            Some(Value::Object(h)) => h.clone(),
857            _ => return false,
858        };
859        let history_borrow = history.borrow();
860        match history_borrow.props.get("_spa_back_stack") {
861            Some(Value::Object(arr)) => {
862                let arr_borrow = arr.borrow();
863                match &arr_borrow.kind {
864                    ObjKind::Array(items) => !items.is_empty(),
865                    _ => false,
866                }
867            }
868            _ => false,
869        }
870    }
871
872    /// SPA back スタックから1エントリ取り出す。history.state・location.href を復元し
873    /// popstate を発火する。戻り値: 復元した URL(呼び元がアドレスバー更新に使う)。
874    pub fn spa_go_back(&mut self) -> Option<String> {
875        let history = match self.global.borrow().vars.get("history") {
876            Some(Value::Object(h)) => h.clone(),
877            _ => return None,
878        };
879        // 現在の URL/state を forward スタックへ退避。
880        {
881            let cur_url = super::builtins::location_href(&self.global).unwrap_or_default();
882            let cur_state = history.borrow().props.get("state").cloned().unwrap_or(Value::Null);
883            let fwd_stack_val = history.borrow().props.get("_spa_fwd_stack").cloned();
884            let fwd_stack = match fwd_stack_val {
885                Some(Value::Object(arr)) => arr,
886                _ => Obj::array(alloc::vec![]),
887            };
888            let entry = Obj::plain();
889            {
890                let mut eb = entry.borrow_mut();
891                eb.props.insert("url".into(), Value::str(cur_url));
892                eb.props.insert("state".into(), cur_state);
893            }
894            if let ObjKind::Array(items) = &mut fwd_stack.borrow_mut().kind {
895                items.push(Value::Object(entry));
896            }
897            history.borrow_mut().props.insert("_spa_fwd_stack".into(), Value::Object(fwd_stack));
898        }
899        // back スタックから pop。
900        let (prev_url, prev_state) = {
901            let back_stack_val = history.borrow().props.get("_spa_back_stack").cloned();
902            let back_stack = match back_stack_val {
903                Some(Value::Object(arr)) => arr,
904                _ => return None,
905            };
906            let entry = match &mut back_stack.borrow_mut().kind {
907                ObjKind::Array(items) => items.pop(),
908                _ => None,
909            }?;
910            let eb = match entry { Value::Object(o) => o, _ => return None };
911            let url = eb.borrow().props.get("url").cloned().unwrap_or(Value::Undefined).to_js_string();
912            let state = eb.borrow().props.get("state").cloned().unwrap_or(Value::Null);
913            (url, state)
914        };
915        // history.state と location を復元。
916        history.borrow_mut().props.insert("state".into(), prev_state);
917        super::builtins::update_location(&self.global, &prev_url);
918        self.base_url = prev_url.to_string();
919        self.fire_popstate();
920        Some(prev_url.to_string())
921    }
922
923    /// SPA forward スタックから1エントリ取り出す。戻り値: 復元した URL。
924    pub fn spa_go_forward(&mut self) -> Option<String> {
925        let history = match self.global.borrow().vars.get("history") {
926            Some(Value::Object(h)) => h.clone(),
927            _ => return None,
928        };
929        // 現在の URL/state を back スタックへ退避。
930        {
931            let cur_url = super::builtins::location_href(&self.global).unwrap_or_default();
932            let cur_state = history.borrow().props.get("state").cloned().unwrap_or(Value::Null);
933            let back_stack_val = history.borrow().props.get("_spa_back_stack").cloned();
934            let back_stack = match back_stack_val {
935                Some(Value::Object(arr)) => arr,
936                _ => Obj::array(alloc::vec![]),
937            };
938            let entry = Obj::plain();
939            {
940                let mut eb = entry.borrow_mut();
941                eb.props.insert("url".into(), Value::str(cur_url));
942                eb.props.insert("state".into(), cur_state);
943            }
944            if let ObjKind::Array(items) = &mut back_stack.borrow_mut().kind {
945                items.push(Value::Object(entry));
946            }
947            history.borrow_mut().props.insert("_spa_back_stack".into(), Value::Object(back_stack));
948        }
949        // forward スタックから pop。
950        let (next_url, next_state) = {
951            let fwd_stack_val = history.borrow().props.get("_spa_fwd_stack").cloned();
952            let fwd_stack = match fwd_stack_val {
953                Some(Value::Object(arr)) => arr,
954                _ => return None,
955            };
956            let entry = match &mut fwd_stack.borrow_mut().kind {
957                ObjKind::Array(items) => items.pop(),
958                _ => None,
959            }?;
960            let eb = match entry { Value::Object(o) => o, _ => return None };
961            let url = eb.borrow().props.get("url").cloned().unwrap_or(Value::Undefined).to_js_string();
962            let state = eb.borrow().props.get("state").cloned().unwrap_or(Value::Null);
963            (url, state)
964        };
965        history.borrow_mut().props.insert("state".into(), next_state);
966        super::builtins::update_location(&self.global, &next_url);
967        self.base_url = next_url.to_string();
968        self.fire_popstate();
969        Some(next_url.to_string())
970    }
971
972    /// SPA forward スタックにエントリがあるか。
973    pub fn has_spa_forward(&self) -> bool {
974        let history = match self.global.borrow().vars.get("history") {
975            Some(Value::Object(h)) => h.clone(),
976            _ => return false,
977        };
978        let history_borrow = history.borrow();
979        match history_borrow.props.get("_spa_fwd_stack") {
980            Some(Value::Object(arr)) => {
981                let arr_borrow = arr.borrow();
982                match &arr_borrow.kind {
983                    ObjKind::Array(items) => !items.is_empty(),
984                    _ => false,
985                }
986            }
987            _ => false,
988        }
989    }
990
991    /// window に登録された scroll リスナを発火する(スクロール位置が変化したときホストが呼ぶ)。
992    /// 併せて window.scrollY / pageYOffset を更新し、Event に scrollY を載せる。
993    pub fn fire_scroll(&mut self, scroll_y: i32) {
994        // scrollY / pageYOffset は常に最新化(リスナの有無に関わらず読み取れるよう)。
995        // window.scrollY(正規)と bare scrollY(グローバル)の両方を同期する。
996        {
997            let g = self.global.borrow();
998            if let Some(Value::Object(w)) = g.vars.get("window") {
999                let mut wb = w.borrow_mut();
1000                wb.props
1001                    .insert("scrollY".into(), Value::Number(scroll_y as f64));
1002                wb.props
1003                    .insert("pageYOffset".into(), Value::Number(scroll_y as f64));
1004            }
1005        }
1006        {
1007            let mut g = self.global.borrow_mut();
1008            g.vars
1009                .insert("scrollY".into(), Value::Number(scroll_y as f64));
1010            g.vars
1011                .insert("pageYOffset".into(), Value::Number(scroll_y as f64));
1012        }
1013        let listeners = self.window_listeners("_scroll_listeners");
1014        self.fire_window_listeners(
1015            &listeners,
1016            "scroll",
1017            &[(String::from("scrollY"), Value::Number(scroll_y as f64))],
1018        );
1019    }
1020
1021    /// window に登録された hashchange リスナを発火する(location.hash 変更 / アンカー(#)ナビ後にホストが呼ぶ)。
1022    /// HashChangeEvent 互換で oldURL / newURL を載せる。併せて location.hash を新値へ更新する。
1023    pub fn fire_hashchange(&mut self, old_url: &str, new_url: &str) {
1024        // location.hash を新URLのフラグメントへ同期(リスナ有無に関わらず読めるよう先に更新)。
1025        let new_hash = match new_url.split_once('#') {
1026            Some((_, frag)) => alloc::format!("#{}", frag),
1027            None => String::new(),
1028        };
1029        {
1030            let g = self.global.borrow();
1031            if let Some(Value::Object(loc)) = g.vars.get("location") {
1032                loc.borrow_mut()
1033                    .props
1034                    .insert("hash".into(), Value::str(&new_hash));
1035            }
1036        }
1037        let listeners = self.window_listeners("_hashchange_listeners");
1038        self.fire_window_listeners(
1039            &listeners,
1040            "hashchange",
1041            &[
1042                (String::from("oldURL"), Value::str(old_url)),
1043                (String::from("newURL"), Value::str(new_url)),
1044            ],
1045        );
1046    }
1047
1048    /// window に登録された resize リスナを発火する(ウィンドウ/ビューポートサイズ変化時にホストが呼ぶ)。
1049    /// 併せて window.innerWidth / innerHeight を更新し、Event に幅・高さを載せる。
1050    pub fn fire_resize(&mut self, width: i32, height: i32) {
1051        {
1052            let g = self.global.borrow();
1053            if let Some(Value::Object(w)) = g.vars.get("window") {
1054                let mut wb = w.borrow_mut();
1055                wb.props
1056                    .insert("innerWidth".into(), Value::Number(width as f64));
1057                wb.props
1058                    .insert("innerHeight".into(), Value::Number(height as f64));
1059            }
1060        }
1061        {
1062            // window 経由でなく素の innerWidth/innerHeight を読むスクリプトとも整合させる。
1063            let mut g = self.global.borrow_mut();
1064            g.vars
1065                .insert("innerWidth".into(), Value::Number(width as f64));
1066            g.vars
1067                .insert("innerHeight".into(), Value::Number(height as f64));
1068        }
1069        let listeners = self.window_listeners("_resize_listeners");
1070        self.fire_window_listeners(
1071            &listeners,
1072            "resize",
1073            &[
1074                (String::from("innerWidth"), Value::Number(width as f64)),
1075                (String::from("innerHeight"), Value::Number(height as f64)),
1076            ],
1077        );
1078    }
1079}
1080
1081/// 1 回の評価セッションの実行器(ステップ予算等を保持)。
1082pub struct Interp {
1083    pub steps: u64,
1084    pub max_steps: u64,
1085    pub depth: u32,
1086    pub max_depth: u32,
1087    pub aborted: bool,
1088    pub out: String,
1089    /// DOM ブリッジ(ネイティブ DOM メソッドが借用して読み書きする)。
1090    pub dom: Rc<RefCell<DomBridge>>,
1091    /// Promise マイクロタスクキュー(ランタイムと共有)。
1092    pub microtasks: Rc<RefCell<VecDeque<Job>>>,
1093    /// generator 本体を replay 実行中の一時状態(None なら通常実行)。
1094    gen_replay: Option<GenReplay>,
1095    /// setTimeout のマクロタスクキュー(ランタイムと共有)。
1096    pub macrotasks: Rc<RefCell<VecDeque<(Value, Vec<Value>, u64)>>>,
1097    /// 現在ページの絶対URL(fetch/XHR の相対URL解決の基準。未設定なら空)。
1098    pub base_url: String,
1099    /// グローバルスコープ(location/history などネイティブ側からの参照用)。
1100    pub global: Rc<RefCell<Scope>>,
1101    /// IntersectionObserver コールバックキュー(ページ実行後に fire_intersection_observers で発火)。
1102    pub io_callbacks: Vec<Value>,
1103    /// setInterval 登録テーブル: (id, callback, extra_args, remaining_fires)
1104    pub intervals: Vec<(u64, Value, Vec<Value>, u32)>,
1105    /// 登録済み ES モジュール(ランタイムと共有)。
1106    pub modules: ModuleRegistry,
1107    /// `Statement::Labeled` がループ文を実行する直前にセットする、そのループ自身の
1108    /// ラベル名(`break label`/`continue label` が「自分宛て」かどうかの判定に使う)。
1109    /// ループの実行開始直後に `take()` で消費するため、ネストしたループ/ラベルには
1110    /// 影響しない(exec_stmt のシグネチャにラベル引数を追加する代わりの軽量な方式)。
1111    pending_label: Option<String>,
1112    /// `construct_object()` が `new` 経由の呼び出し直前にセットする、その呼び出し先
1113    /// コンストラクタ自身の値。`call_value_inner` の `CallKind::User` 実行開始時に
1114    /// `take()` して `new_target_stack` へ push する(`pending_label` と同じ軽量な方式)。
1115    pending_new_target: Option<Value>,
1116    /// `new.target`(MetaProperty)の現在値のスタック。関数呼び出しごとに
1117    /// (アロー関数を除き)push/pop する。空なら(モジュールトップレベル等)`undefined`。
1118    new_target_stack: Vec<Value>,
1119}
1120
1121type EvalResult = Result<Value, Value>;
1122
1123
1124// ===== impl Interp 分割サブモジュール(2026-07-16 リファクタ フェーズ5) =====
1125// 約5,950行の単一 impl Interp を機能グループ別に interp/ 配下へ分割。各ファイルは
1126// `impl Interp { ... }` を持ち、メソッドは型 Interp に自動結合する(パス参照は無いため
1127// use 再エクスポート不要)。詳細は walkthrough.md 参照。
1128mod exec;
1129mod iter_gen;
1130mod eval_expr;
1131mod promise_loop;
1132mod operators;
1133mod calls;
1134mod properties;
1135mod dom_props;
1136
1137
1138/// ARIA 反映 IDL 属性(`element.role`/`.ariaLabel`/`.ariaValueNow` 等)の
1139/// プロパティ名 → 実際の content 属性名の対応表。`aria-valuenow` のように
1140/// 複合語内部にハイフンを持たない属性が多いため、`dataset` と同じ単純な
1141/// camelCase⇄kebab-case 変換は使えず、固定表で引く(丸ごと未対応だった)。
1142/// ここに無い `aria*` プロパティは対象外(低頻度のものは既知の未対応のまま)。
1143fn aria_attr_name(prop: &str) -> Option<&'static str> {
1144    Some(match prop {
1145        "role" => "role",
1146        "ariaLabel" => "aria-label",
1147        "ariaLabelledBy" => "aria-labelledby",
1148        "ariaDescribedBy" => "aria-describedby",
1149        "ariaHidden" => "aria-hidden",
1150        "ariaExpanded" => "aria-expanded",
1151        "ariaChecked" => "aria-checked",
1152        "ariaSelected" => "aria-selected",
1153        "ariaDisabled" => "aria-disabled",
1154        "ariaPressed" => "aria-pressed",
1155        "ariaCurrent" => "aria-current",
1156        "ariaLive" => "aria-live",
1157        "ariaBusy" => "aria-busy",
1158        "ariaRequired" => "aria-required",
1159        "ariaInvalid" => "aria-invalid",
1160        "ariaValueNow" => "aria-valuenow",
1161        "ariaValueMin" => "aria-valuemin",
1162        "ariaValueMax" => "aria-valuemax",
1163        "ariaValueText" => "aria-valuetext",
1164        "ariaControls" => "aria-controls",
1165        "ariaOwns" => "aria-owns",
1166        "ariaModal" => "aria-modal",
1167        "ariaMultiline" => "aria-multiline",
1168        "ariaMultiSelectable" => "aria-multiselectable",
1169        "ariaOrientation" => "aria-orientation",
1170        "ariaPlaceholder" => "aria-placeholder",
1171        "ariaReadOnly" => "aria-readonly",
1172        "ariaRoleDescription" => "aria-roledescription",
1173        "ariaSort" => "aria-sort",
1174        "ariaAtomic" => "aria-atomic",
1175        "ariaHasPopup" => "aria-haspopup",
1176        // 表・リスト系でよく使われる残りの ARIA 反映 IDL 属性が丸ごと未対応
1177        // だった(`role`/`ariaLabel`等と同じ固定表への追加のみで済む対応)。
1178        "ariaColCount" => "aria-colcount",
1179        "ariaColIndex" => "aria-colindex",
1180        "ariaColSpan" => "aria-colspan",
1181        "ariaRowCount" => "aria-rowcount",
1182        "ariaRowIndex" => "aria-rowindex",
1183        "ariaRowSpan" => "aria-rowspan",
1184        "ariaSetSize" => "aria-setsize",
1185        "ariaPosInSet" => "aria-posinset",
1186        "ariaLevel" => "aria-level",
1187        "ariaKeyShortcuts" => "aria-keyshortcuts",
1188        "ariaAutoComplete" => "aria-autocomplete",
1189        "ariaDetails" => "aria-details",
1190        "ariaErrorMessage" => "aria-errormessage",
1191        "ariaFlowTo" => "aria-flowto",
1192        "ariaRelevant" => "aria-relevant",
1193        _ => return None,
1194    })
1195}
1196
1197/// camelCase を kebab-case へ変換。`backgroundColor` → `background-color`。
1198/// 既に kebab(`-` 含む)や小文字のみならほぼそのまま。
1199fn camel_to_kebab(key: &str) -> String {
1200    let mut s = String::new();
1201    for c in key.chars() {
1202        if c.is_ascii_uppercase() {
1203            s.push('-');
1204            s.push(c.to_ascii_lowercase());
1205        } else {
1206            s.push(c);
1207        }
1208    }
1209    s
1210}
1211
1212/// `dataset` のキー(camelCase)を `data-kebab-case` 属性名へ変換。`userId` → `data-user-id`。
1213fn camel_to_data_attr(key: &str) -> String {
1214    let mut s = String::from("data-");
1215    for c in key.chars() {
1216        if c.is_ascii_uppercase() {
1217            s.push('-');
1218            s.push(c.to_ascii_lowercase());
1219        } else {
1220            s.push(c);
1221        }
1222    }
1223    s
1224}
1225
1226/// DOM プロキシの種別(借用を跨がないよう kind を取り出した結果)。
1227pub(crate) enum DomDisp {
1228    Element(usize),
1229    Host(String),
1230}
1231
1232enum CallKind {
1233    User(FunctionData),
1234    Native(NativeFn),
1235    Resolver(Rc<RefCell<PromiseState>>, bool),
1236}
1237
1238// ============ ヘルパ ============
1239
1240/// イテラブル(配列/文字列/Set/Map)を Value 列へ展開する。スプレッド・for-of 共通。
1241pub fn iterable_values(v: &Value) -> Vec<Value> {
1242    match v {
1243        Value::Str(s) => s.chars().map(|c| Value::str(c.to_string())).collect(),
1244        Value::Object(o) => {
1245            let b = o.borrow();
1246            match &b.kind {
1247                ObjKind::Array(items) => items.clone(),
1248                ObjKind::SetObj(items) => items.clone(),
1249                ObjKind::MapObj(entries) => entries
1250                    .iter()
1251                    .map(|(k, val)| Value::Object(Obj::array(alloc::vec![k.clone(), val.clone()])))
1252                    .collect(),
1253                // generator は遅延駆動が必要(&mut Interp)。この純関数では展開できないため
1254                // 空を返す。generator を展開したい呼び出し側は Interp::iter_to_vec を使うこと。
1255                ObjKind::Generator(_) => Vec::new(),
1256                // `entries`/`keys`/`values` が返す軽量イテレータ(`make_iterator()`。
1257                // `Obj::host("iterator")` + 内部 `_items`/`_pos`)。以前はここが未対応で
1258                // `_ => Vec::new()` に落ち、`new Map(arr.entries())`/`new Set(arr.values())`
1259                // が常に空になるバグだった(この関数はメソッド呼出しをしない純関数のため
1260                // `next()` は呼ばず、内部状態を直接読む)。
1261                ObjKind::Host(t) if t == "iterator" => {
1262                    let items = b.props.get("_items").cloned();
1263                    let pos = b
1264                        .props
1265                        .get("_pos")
1266                        .map(|v| v.to_number() as usize)
1267                        .unwrap_or(0);
1268                    match items {
1269                        Some(Value::Object(arr)) => match &arr.borrow().kind {
1270                            ObjKind::Array(v) => v.get(pos..).map(|s| s.to_vec()).unwrap_or_default(),
1271                            _ => Vec::new(),
1272                        },
1273                        _ => Vec::new(),
1274                    }
1275                }
1276                _ => Vec::new(),
1277            }
1278        }
1279        _ => Vec::new(),
1280    }
1281}
1282
1283fn to_property_key(v: &Value) -> String {
1284    match v {
1285        Value::Number(n) => fmt_number(*n),
1286        Value::Str(s) => (**s).clone(),
1287        _ => v.to_js_string(),
1288    }
1289}
1290
1291/// BigInt のシフト量を i64 に変換する。f64 経由で安全範囲(|n| < 2^31)に収まるもののみ Some。
1292/// 過大なシフトは現実的なメモリ上限を超えるため None を返して呼び出し側でエラーにする。
1293fn shift_amount(b: &super::bigint::BigInt) -> Option<i64> {
1294    let f = b.to_f64();
1295    if !f.is_finite() {
1296        return None;
1297    }
1298    // 2^31 ビット(約 2.6 億バイト)を超えるシフトは拒否。
1299    if libm::fabs(f) >= 2147483648.0 {
1300        return None;
1301    }
1302    Some(f as i64)
1303}
1304
1305fn to_i32(n: f64) -> i32 {
1306    if !n.is_finite() {
1307        return 0;
1308    }
1309    let m = libm::trunc(n);
1310    (m as i64 as u32) as i32
1311}
1312fn to_u32(n: f64) -> u32 {
1313    to_i32(n) as u32
1314}
1315
1316fn powf(base: f64, exp: f64) -> f64 {
1317    // no_std: libm 相当が無いので簡易実装(整数指数を優先)。
1318    if exp == 0.0 {
1319        return 1.0;
1320    }
1321    // 非整数や大きな指数は libm に委譲。
1322    libm::pow(base, exp)
1323}
1324
1325fn loose_eq(l: &Value, r: &Value) -> bool {
1326    match (l, r) {
1327        (Value::Null, Value::Undefined) | (Value::Undefined, Value::Null) => true,
1328        (Value::Null, Value::Null) | (Value::Undefined, Value::Undefined) => true,
1329        (Value::Number(a), Value::Number(b)) => a == b,
1330        (Value::Str(a), Value::Str(b)) => a == b,
1331        (Value::Bool(a), Value::Bool(b)) => a == b,
1332        (Value::Object(a), Value::Object(b)) => Rc::ptr_eq(a, b),
1333        // Number/String と Object の比較: ObjKind::DateObj 以外は仕様上 ToPrimitive で
1334        // `toString()` に落ちるため、その文字列を相手側と再比較する(`[5]==5`/`[5]=='5'` が
1335        // 以前はどの分岐にも該当せず常に `false` になっていたバグ)。DateObj は既存の
1336        // `to_number()` 特殊扱いにより Number 側との比較のみここで数値として素通しする。
1337        (Value::Object(o), other) | (other, Value::Object(o))
1338            if !matches!(other, Value::Object(_)) =>
1339        {
1340            if matches!(o.borrow().kind, ObjKind::DateObj(_)) {
1341                l.to_number() == r.to_number()
1342            } else {
1343                loose_eq(&Value::str(Value::Object(o.clone()).to_js_string()), other)
1344            }
1345        }
1346        // 異なる型: 数値化して比較(オブジェクト除く)。
1347        (Value::Number(_), Value::Str(_))
1348        | (Value::Str(_), Value::Number(_))
1349        | (Value::Bool(_), _)
1350        | (_, Value::Bool(_)) => l.to_number() == r.to_number(),
1351        _ => false,
1352    }
1353}
1354
1355/// 2 つの f64 を比較。NaN が絡むと None。
1356fn cmp_f64(a: f64, b: f64) -> Option<core::cmp::Ordering> {
1357    if a.is_nan() || b.is_nan() {
1358        return None;
1359    }
1360    Some(if a < b {
1361        core::cmp::Ordering::Less
1362    } else if a > b {
1363        core::cmp::Ordering::Greater
1364    } else {
1365        core::cmp::Ordering::Equal
1366    })
1367}
1368
1369fn cmp(l: &Value, r: &Value, pred: fn(core::cmp::Ordering) -> bool) -> Value {
1370    // 両方文字列なら辞書順、そうでなければ数値比較。
1371    if let (Value::Str(a), Value::Str(b)) = (l, r) {
1372        return Value::Bool(pred(a.as_str().cmp(b.as_str())));
1373    }
1374    let a = l.to_number();
1375    let b = r.to_number();
1376    if a.is_nan() || b.is_nan() {
1377        return Value::Bool(false);
1378    }
1379    let ord = if a < b {
1380        core::cmp::Ordering::Less
1381    } else if a > b {
1382        core::cmp::Ordering::Greater
1383    } else {
1384        core::cmp::Ordering::Equal
1385    };
1386    Value::Bool(pred(ord))
1387}