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