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