Skip to main content

atmos/os_lib/js/
value.rs

1//! JS ランタイム値の表現。
2//!
3//! ヒープオブジェクトは `Rc<RefCell<Obj>>` で表現する(参照カウントGC)。
4//! 循環参照はリークするが、ページ遷移ごとに `JsRuntime` を破棄するため許容する。
5
6use alloc::boxed::Box;
7use indexmap::IndexMap;
8use alloc::format;
9use alloc::rc::Rc;
10use alloc::string::String;
11use alloc::vec::Vec;
12use core::cell::RefCell;
13
14use super::ast::{Param, Statement};
15use super::bigint::BigInt;
16use super::interp::{Interp, Scope};
17
18/// FNV-1a ハッシャー。`IndexMap`(`props`の列挙順序保持のため`BTreeMap`から
19/// 移行。ES仕様の`OrdinaryOwnPropertyKeys`が要求する挿入順保持のため)の
20/// デフォルトハッシャー`RandomState`は`std`のOS乱数取得に依存しており、
21/// `default-features = false`にした`no_std`ビルドでは提供されない
22/// (`IndexMap::new()`が使えずビルド不能になっていた)。プロパティ名の
23/// ハッシュ化にセキュリティ上のDoS耐性(乱数シードによるハッシュ衝突
24/// 攻撃対策)は不要なため、決定的な軽量ハッシュ関数で足りる。
25#[derive(Default)]
26pub struct FnvHasher(u64);
27impl core::hash::Hasher for FnvHasher {
28    fn finish(&self) -> u64 {
29        self.0
30    }
31    fn write(&mut self, bytes: &[u8]) {
32        const FNV_OFFSET: u64 = 0xcbf29ce484222325;
33        const FNV_PRIME: u64 = 0x100000001b3;
34        let mut hash = if self.0 == 0 { FNV_OFFSET } else { self.0 };
35        for &b in bytes {
36            hash ^= b as u64;
37            hash = hash.wrapping_mul(FNV_PRIME);
38        }
39        self.0 = hash;
40    }
41}
42/// `Obj::props`/`.accessors`/`.attrs`で使う`IndexMap`の型省略用エイリアス。
43pub type PropMap<V> = IndexMap<String, V, core::hash::BuildHasherDefault<FnvHasher>>;
44
45/// ヒープ上のオブジェクトへの共有参照。
46pub type ObjRef = Rc<RefCell<Obj>>;
47
48/// ネイティブ関数のシグネチャ。`Err(Value)` は JS の throw を表す。
49pub type NativeFn = fn(&mut Interp, this: Value, args: &[Value]) -> Result<Value, Value>;
50
51/// JS の値。プリミティブ + オブジェクト参照。
52#[derive(Clone)]
53pub enum Value {
54    Undefined,
55    Null,
56    Bool(bool),
57    Number(f64),
58    Str(Rc<String>),
59    /// 任意精度整数 BigInt。
60    BigInt(Rc<BigInt>),
61    Object(ObjRef),
62}
63
64/// ユーザー定義関数のデータ。
65#[derive(Clone)]
66pub struct FunctionData {
67    pub name: String,
68    pub params: Vec<Param>,
69    pub body: Rc<Vec<Statement>>,
70    /// クロージャが捕捉した定義時スコープ。
71    pub closure: Rc<RefCell<Scope>>,
72    /// アロー関数か(`this` を捕捉し、自身の `this`/`arguments` を持たない)。
73    pub is_arrow: bool,
74    /// async 関数か(呼び出すと Promise を返す)。
75    pub is_async: bool,
76    /// generator 関数か(呼び出すと Generator を返す)。
77    pub is_generator: bool,
78    /// アロー関数が捕捉した `this`。
79    pub bound_this: Option<Box<Value>>,
80}
81
82/// オブジェクトの種別。Plain 以外は内部表現を持つ。
83#[derive(Clone)]
84pub enum ObjKind {
85    Plain,
86    Array(Vec<Value>),
87    Function(FunctionData),
88    Native {
89        name: String,
90        func: NativeFn,
91    },
92    /// DOM 要素ハンドル(DOMアリーナのノード index を保持)。
93    DomElement(usize),
94    /// `document` などのホストオブジェクト識別子。
95    Host(String),
96    /// Map: 挿入順を保つ (key, value) リスト(キー等価は SameValueZero 近似)。
97    MapObj(Vec<(Value, Value)>),
98    /// Set: 挿入順を保つ値リスト。
99    SetObj(Vec<Value>),
100    /// Promise。状態は共有可変(then/await で参照される)。
101    PromiseObj(Rc<RefCell<PromiseState>>),
102    /// resolve/reject 関数(Promise 状態を束縛したクロージャ相当)。
103    Resolver {
104        state: Rc<RefCell<PromiseState>>,
105        reject: bool,
106    },
107    /// Generator(遅延評価。resume のたびに本体を replay して次の yield まで進める)。
108    Generator(Rc<RefCell<GenState>>),
109    /// RegExp(コンパイル済みパターン + lastIndex)。
110    RegExpObj(Rc<RefCell<super::regex::RegExpData>>),
111    /// Proxy(target とハンドラ。トラップ未定義時は target にフォワード)。
112    Proxy {
113        target: ObjRef,
114        handler: ObjRef,
115    },
116    /// `Function.prototype.bind()` が返す束縛済み関数。呼び出し時は
117    /// `bound_args ++ 実引数` を `bound_this` を this として `target` に渡す。
118    Bound {
119        target: Value,
120        bound_this: Value,
121        bound_args: Vec<Value>,
122    },
123    /// `Date`。内部値は UTC エポックミリ秒(不正な日時は `f64::NAN`。仕様の Invalid Date 相当)。
124    DateObj(f64),
125}
126
127/// Generator の完了・再開種別。
128#[derive(Clone)]
129pub enum GenCompletion {
130    Normal(Value),
131    Throw(Value),
132    Return(Value),
133}
134
135/// Generator の状態(遅延評価 / replay 方式)。
136///
137/// 真のコルーチン(スタック退避)は no_std ツリーウォーク実装では困難なため、
138/// resume のたびに関数本体を先頭から再実行(replay)し、N 番目の yield で中断する。
139/// 既に通過した yield には `sent`(`.next(v)` で渡された値)を返すことで
140/// 双方向の値受け渡しと無限ジェネレータを実現する。
141///
142/// 限界: replay のため本体の副作用(console.log 等)は resume ごとに再実行される。
143pub struct GenState {
144    /// generator 関数本体(params/body/closure を保持)。
145    pub func: FunctionData,
146    /// 呼び出し時の実引数。
147    pub args: Vec<Value>,
148    /// 呼び出し時の `this`。
149    pub this: Value,
150    /// 既に通過した各 yield に `.next(v)`/`.throw(e)`/`.return(v)` で渡された結果。
151    pub sent: Vec<GenCompletion>,
152    /// 最初の `.next()` を消費済みか(最初の next の引数は捨てられる)。
153    pub started: bool,
154    /// 完了済みか(以降の next は常に done:true)。
155    pub done: bool,
156    /// 完了時の return 値(done:true の value)。
157    pub returned: Value,
158}
159
160#[derive(Debug, Clone, Copy, PartialEq)]
161pub struct PropertyAttributes {
162    pub writable: bool,
163    pub configurable: bool,
164    pub enumerable: bool,
165}
166
167impl Default for PropertyAttributes {
168    fn default() -> Self {
169        PropertyAttributes {
170            writable: true,
171            configurable: true,
172            enumerable: true,
173        }
174    }
175}
176
177/// Promise の状態。
178#[derive(Clone, Copy, PartialEq)]
179pub enum PromiseStatus {
180    Pending,
181    Fulfilled,
182    Rejected,
183}
184
185/// settle 時に消費する then 反応。
186pub struct Reaction {
187    pub on_fulfilled: Option<Value>,
188    pub on_rejected: Option<Value>,
189    /// この反応の結果を反映する派生 Promise。
190    pub result: Rc<RefCell<PromiseState>>,
191}
192
193pub struct PromiseState {
194    pub status: PromiseStatus,
195    /// fulfilled の値、または rejected の理由。
196    pub value: Value,
197    /// pending 中に登録された反応(settle 時にマイクロタスクへ)。
198    pub reactions: Vec<Reaction>,
199}
200
201impl PromiseState {
202    pub fn pending() -> Self {
203        PromiseState {
204            status: PromiseStatus::Pending,
205            value: Value::Undefined,
206            reactions: Vec::new(),
207        }
208    }
209}
210
211/// マイクロタスク(then 反応の遅延実行単位)。
212pub struct Job {
213    /// 実行するハンドラ(None なら値をそのまま伝播)。
214    pub handler: Option<Value>,
215    pub arg: Value,
216    pub is_fulfill: bool,
217    pub result: Rc<RefCell<PromiseState>>,
218}
219
220/// アクセサ記述子(get/set の一方または両方)。
221pub struct Accessor {
222    pub get: Option<Value>,
223    pub set: Option<Value>,
224}
225
226/// ヒープオブジェクト本体。名前付きプロパティ + プロトタイプ + 種別。
227pub struct Obj {
228    pub props: PropMap<Value>,
229    /// Object.defineProperty で登録された getter/setter。
230    pub accessors: PropMap<Accessor>,
231    pub attrs: PropMap<PropertyAttributes>,
232    pub proto: Option<ObjRef>,
233    pub kind: ObjKind,
234    /// `Object.freeze()` 済みか。真なら `set_property`/`delete` によるプロパティの
235    /// 追加・変更・削除を無視する(非 strict モード相当の黙殺。`Object.isFrozen()` が読む)。
236    pub frozen: bool,
237    /// `Object.seal()` 済みか。真なら新規プロパティの追加・既存プロパティの削除を無視する
238    /// (既存プロパティの値の変更は許可。`frozen` はこれも自動的に含意する)。
239    pub sealed: bool,
240    /// `Object.preventExtensions()` 済みか。真なら新規プロパティの追加のみを禁止する
241    /// (削除・既存値の変更は許可。`sealed`/`frozen` はこれも自動的に含意する)。
242    pub non_extensible: bool,
243    /// `Object.create(null)`/`Object.groupBy()`等、仕様上`[[Prototype]]`が
244    /// `null`と明示されたオブジェクトか。`proto: None`だけでは「明示的に
245    /// nullプロトタイプ」なのか「単に通常オブジェクトでprotoチェーンが
246    /// 尽きた」なのかを`get_property`の共通メソッドフォールバック
247    /// (`interp/properties.rs`の`object_get`呼び出し)が区別できず、
248    /// `Object.create(null).hasOwnProperty`等がフォールバック経由で
249    /// 誤って動いてしまうバグがあった(2026-07-18 発見・実装)。
250    pub null_proto: bool,
251}
252
253impl Obj {
254    pub fn plain() -> ObjRef {
255        Rc::new(RefCell::new(Obj {
256            props: PropMap::default(),
257            accessors: PropMap::default(),
258            attrs: PropMap::default(),
259            proto: None,
260            frozen: false,
261            sealed: false,
262            non_extensible: false,
263            null_proto: false,
264            kind: ObjKind::Plain,
265        }))
266    }
267    pub fn array(items: Vec<Value>) -> ObjRef {
268        Rc::new(RefCell::new(Obj {
269            props: PropMap::default(),
270            accessors: PropMap::default(),
271            attrs: PropMap::default(),
272            proto: None,
273            frozen: false,
274            sealed: false,
275            non_extensible: false,
276            null_proto: false,
277            kind: ObjKind::Array(items),
278        }))
279    }
280    pub fn function(data: FunctionData) -> ObjRef {
281        Rc::new(RefCell::new(Obj {
282            props: PropMap::default(),
283            accessors: PropMap::default(),
284            attrs: PropMap::default(),
285            proto: None,
286            frozen: false,
287            sealed: false,
288            non_extensible: false,
289            null_proto: false,
290            kind: ObjKind::Function(data),
291        }))
292    }
293    pub fn native(name: &str, func: NativeFn) -> ObjRef {
294        Rc::new(RefCell::new(Obj {
295            props: PropMap::default(),
296            accessors: PropMap::default(),
297            attrs: PropMap::default(),
298            proto: None,
299            frozen: false,
300            sealed: false,
301            non_extensible: false,
302            null_proto: false,
303            kind: ObjKind::Native {
304                name: String::from(name),
305                func,
306            },
307        }))
308    }
309    pub fn host(tag: &str) -> ObjRef {
310        Rc::new(RefCell::new(Obj {
311            props: PropMap::default(),
312            accessors: PropMap::default(),
313            attrs: PropMap::default(),
314            proto: None,
315            frozen: false,
316            sealed: false,
317            non_extensible: false,
318            null_proto: false,
319            kind: ObjKind::Host(String::from(tag)),
320        }))
321    }
322    pub fn dom(node_idx: usize) -> ObjRef {
323        Rc::new(RefCell::new(Obj {
324            props: PropMap::default(),
325            accessors: PropMap::default(),
326            attrs: PropMap::default(),
327            proto: None,
328            frozen: false,
329            sealed: false,
330            non_extensible: false,
331            null_proto: false,
332            kind: ObjKind::DomElement(node_idx),
333        }))
334    }
335    pub fn map_obj(entries: Vec<(Value, Value)>) -> ObjRef {
336        Rc::new(RefCell::new(Obj {
337            props: PropMap::default(),
338            accessors: PropMap::default(),
339            attrs: PropMap::default(),
340            proto: None,
341            frozen: false,
342            sealed: false,
343            non_extensible: false,
344            null_proto: false,
345            kind: ObjKind::MapObj(entries),
346        }))
347    }
348    pub fn set_obj(items: Vec<Value>) -> ObjRef {
349        Rc::new(RefCell::new(Obj {
350            props: PropMap::default(),
351            accessors: PropMap::default(),
352            attrs: PropMap::default(),
353            proto: None,
354            frozen: false,
355            sealed: false,
356            non_extensible: false,
357            null_proto: false,
358            kind: ObjKind::SetObj(items),
359        }))
360    }
361    pub fn promise(state: Rc<RefCell<PromiseState>>) -> ObjRef {
362        Rc::new(RefCell::new(Obj {
363            props: PropMap::default(),
364            accessors: PropMap::default(),
365            attrs: PropMap::default(),
366            proto: None,
367            frozen: false,
368            sealed: false,
369            non_extensible: false,
370            null_proto: false,
371            kind: ObjKind::PromiseObj(state),
372        }))
373    }
374    pub fn resolver(state: Rc<RefCell<PromiseState>>, reject: bool) -> ObjRef {
375        Rc::new(RefCell::new(Obj {
376            props: PropMap::default(),
377            accessors: PropMap::default(),
378            attrs: PropMap::default(),
379            proto: None,
380            frozen: false,
381            sealed: false,
382            non_extensible: false,
383            null_proto: false,
384            kind: ObjKind::Resolver { state, reject },
385        }))
386    }
387    pub fn generator(state: GenState) -> ObjRef {
388        Rc::new(RefCell::new(Obj {
389            props: PropMap::default(),
390            accessors: PropMap::default(),
391            attrs: PropMap::default(),
392            proto: None,
393            frozen: false,
394            sealed: false,
395            non_extensible: false,
396            null_proto: false,
397            kind: ObjKind::Generator(Rc::new(RefCell::new(state))),
398        }))
399    }
400    pub fn regexp(data: super::regex::RegExpData) -> ObjRef {
401        Rc::new(RefCell::new(Obj {
402            props: PropMap::default(),
403            accessors: PropMap::default(),
404            attrs: PropMap::default(),
405            proto: None,
406            frozen: false,
407            sealed: false,
408            non_extensible: false,
409            null_proto: false,
410            kind: ObjKind::RegExpObj(Rc::new(RefCell::new(data))),
411        }))
412    }
413    pub fn proxy(target: ObjRef, handler: ObjRef) -> ObjRef {
414        Rc::new(RefCell::new(Obj {
415            props: PropMap::default(),
416            accessors: PropMap::default(),
417            attrs: PropMap::default(),
418            proto: None,
419            frozen: false,
420            sealed: false,
421            non_extensible: false,
422            null_proto: false,
423            kind: ObjKind::Proxy { target, handler },
424        }))
425    }
426    pub fn date_obj(epoch_ms: f64) -> ObjRef {
427        Rc::new(RefCell::new(Obj {
428            props: PropMap::default(),
429            accessors: PropMap::default(),
430            attrs: PropMap::default(),
431            proto: None,
432            frozen: false,
433            sealed: false,
434            non_extensible: false,
435            null_proto: false,
436            kind: ObjKind::DateObj(epoch_ms),
437        }))
438    }
439    pub fn bound(target: Value, bound_this: Value, bound_args: Vec<Value>) -> ObjRef {
440        Rc::new(RefCell::new(Obj {
441            props: PropMap::default(),
442            accessors: PropMap::default(),
443            attrs: PropMap::default(),
444            proto: None,
445            frozen: false,
446            sealed: false,
447            non_extensible: false,
448            null_proto: false,
449            kind: ObjKind::Bound {
450                target,
451                bound_this,
452                bound_args,
453            },
454        }))
455    }
456    pub fn is_callable(&self) -> bool {
457        match &self.kind {
458            ObjKind::Function(_) | ObjKind::Native { .. } | ObjKind::Resolver { .. } => true,
459            ObjKind::Bound { .. } => true,
460            // Proxy は target が callable なら callable。
461            ObjKind::Proxy { target, .. } => target.borrow().is_callable(),
462            _ => false,
463        }
464    }
465}
466
467impl Value {
468    pub fn str(s: impl Into<String>) -> Value {
469        Value::Str(Rc::new(s.into()))
470    }
471    pub fn bigint(b: BigInt) -> Value {
472        Value::BigInt(Rc::new(b))
473    }
474    pub fn object(o: ObjRef) -> Value {
475        Value::Object(o)
476    }
477
478    /// ECMAScript ToBoolean。
479    pub fn truthy(&self) -> bool {
480        match self {
481            Value::Undefined | Value::Null => false,
482            Value::Bool(b) => *b,
483            Value::Number(n) => *n != 0.0 && !n.is_nan(),
484            Value::Str(s) => !s.is_empty(),
485            Value::BigInt(b) => !b.is_zero(),
486            Value::Object(_) => true,
487        }
488    }
489
490    /// ECMAScript ToNumber(簡易)。
491    pub fn to_number(&self) -> f64 {
492        match self {
493            Value::Undefined => f64::NAN,
494            Value::Null => 0.0,
495            Value::Bool(b) => {
496                if *b {
497                    1.0
498                } else {
499                    0.0
500                }
501            }
502            Value::Number(n) => *n,
503            Value::Str(s) => {
504                let t = s.trim();
505                if t.is_empty() {
506                    0.0
507                } else if let Some(rest) = t.strip_prefix("0x").or_else(|| t.strip_prefix("0X")) {
508                    // `StringNumericLiteral` は `0x`/`0o`/`0b` 整数リテラルも受理する(符号無し)。
509                    // `Number("0x1F")`===31 のような変換が以前は常に `NaN` になっていた。
510                    u64::from_str_radix(rest, 16).map(|n| n as f64).unwrap_or(f64::NAN)
511                } else if let Some(rest) = t.strip_prefix("0o").or_else(|| t.strip_prefix("0O")) {
512                    u64::from_str_radix(rest, 8).map(|n| n as f64).unwrap_or(f64::NAN)
513                } else if let Some(rest) = t.strip_prefix("0b").or_else(|| t.strip_prefix("0B")) {
514                    u64::from_str_radix(rest, 2).map(|n| n as f64).unwrap_or(f64::NAN)
515                } else {
516                    t.parse::<f64>().unwrap_or(f64::NAN)
517                }
518            }
519            Value::BigInt(b) => b.to_f64(),
520            // `Date` は ToPrimitive(hint=number) で `valueOf()`(エポックミリ秒)相当になる
521            // (仕様上は他の型も `valueOf`/`Symbol.toPrimitive` を経由し得るが、この処理系は
522            // それらを汎用的にはサポートしないため `Date` だけ直接特殊扱いする)。
523            // これにより `date2 - date1` や `date1 < date2` の算術/比較が素で動くようになる。
524            // `Array` も ToPrimitive(hint=default/number) で `toString()`(`join(',')`)した
525            // 文字列を数値変換する(`[5]*2`===10、`[]+1`===1 のような定番イディオムが
526            // 以前は常に `NaN` になっていた)。単一要素なら中身がそのまま数値化され、空配列は
527            // `0`、複数要素は通常カンマ区切り文字列がパース不能で `NaN` になる(仕様どおり)。
528            Value::Object(o) => match &o.borrow().kind {
529                ObjKind::DateObj(ms) => *ms,
530                ObjKind::Array(_) => {
531                    let s = self.to_js_string();
532                    let t = s.trim();
533                    if t.is_empty() {
534                        0.0
535                    } else {
536                        t.parse::<f64>().unwrap_or(f64::NAN)
537                    }
538                }
539                // `Array.isArray`/`Object.keys`/`structuredClone`/`to_js_string` 等、
540                // 他の多くの操作は既に target へ委譲する透過性修正済みだったのに、
541                // 数値変換だけ見落とされていた(`+new Proxy([5],{})`===`5`、
542                // `new Proxy([5],{})*2`===`10` のような ToPrimitive(hint=number)
543                // 経由の算術が常に `NaN` になっていたバグ)。
544                ObjKind::Proxy { target, .. } => Value::Object(target.clone()).to_number(),
545                _ => f64::NAN,
546            },
547        }
548    }
549
550    /// 型を表す `typeof` 文字列。
551    pub fn type_of(&self) -> &'static str {
552        match self {
553            Value::Undefined => "undefined",
554            Value::Null => "object",
555            Value::Bool(_) => "boolean",
556            Value::Number(_) => "number",
557            Value::Str(_) => "string",
558            Value::BigInt(_) => "bigint",
559            Value::Object(o) => {
560                if o.borrow().is_callable() {
561                    "function"
562                } else {
563                    "object"
564                }
565            }
566        }
567    }
568
569    /// ECMAScript ToString(簡易・表示用)。
570    pub fn to_js_string(&self) -> String {
571        let mut seen = Vec::new();
572        self.to_js_string_seen(&mut seen)
573    }
574
575    /// `to_js_string` の内部実装。`Array` の要素文字列化は再帰的に自分自身の
576    /// `to_js_string` を呼ぶため、`var a=[1]; a.push(a); a.join()`(あるいは
577    /// `String(a)`/テンプレートリテラル補間等、内部的に同じ経路を通る操作)が
578    /// 循環参照でスタックオーバーフロー(この no_std 環境ではクラッシュ/ハングに
579    /// なり得る)していた。`structuredClone`/`JSON.stringify`/`console.log` で
580    /// 既に修正済みの「祖先を追跡して打ち切る」パターンをここにも適用し、
581    /// 既に祖先に現れた配列を再訪した時点で空文字列を返して打ち切る
582    /// (実ブラウザは無条件のスタックオーバーフローで `RangeError` を投げるが、
583    /// この処理系は例外化されない Rust ネイティブ再帰でのクラッシュを避ける
584    /// ことを優先し、値としては空文字列で打ち切る簡略挙動とする。
585    /// `a=[1]; a.push(a); a.join()` は `"1,1,"` になる——2番目の要素 `a` 自身の
586    /// `to_js_string()` が、その内部で自分自身を再訪した時点(3階層目)で
587    /// 空文字列に切り詰められるため)。
588    fn to_js_string_seen(&self, seen: &mut Vec<ObjRef>) -> String {
589        if seen.len() > 30 {
590            return String::new();
591        }
592        match self {
593            Value::Undefined => String::from("undefined"),
594            Value::Null => String::from("null"),
595            Value::Bool(b) => {
596                if *b {
597                    String::from("true")
598                } else {
599                    String::from("false")
600                }
601            }
602            Value::Number(n) => fmt_number(*n),
603            Value::Str(s) => (**s).clone(),
604            Value::BigInt(b) => b.to_decimal_string(),
605            Value::Object(o) => {
606                let b = o.borrow();
607                match &b.kind {
608                    ObjKind::Array(items) => {
609                        if seen.iter().any(|s| Rc::ptr_eq(s, o)) {
610                            return String::new();
611                        }
612                        seen.push(o.clone());
613                        let parts: Vec<String> = items
614                            .iter()
615                            .map(|v| match v {
616                                Value::Undefined | Value::Null => String::new(),
617                                _ => v.to_js_string_seen(seen),
618                            })
619                            .collect();
620                        seen.pop();
621                        parts.join(",")
622                    }
623                    ObjKind::Function(f) => format!("function {}() {{ [code] }}", f.name),
624                    ObjKind::Native { name, .. } => {
625                        format!("function {}() {{ [native code] }}", name)
626                    }
627                    // `fn.bind(...)` が返す束縛済み関数(`ObjKind::Bound`)を
628                    // 文字列化すると、専用ケースが無く汎用の `[object Object]` に
629                    // 落ちるバグだった(`.name`/`.length` の欠落と同種)。
630                    ObjKind::Bound { .. } => String::from("function () { [native code] }"),
631                    ObjKind::MapObj(_) => String::from("[object Map]"),
632                    ObjKind::SetObj(_) => String::from("[object Set]"),
633                    ObjKind::PromiseObj(_) => String::from("[object Promise]"),
634                    ObjKind::Resolver { .. } => String::from("function () { [native code] }"),
635                    ObjKind::Generator(_) => String::from("[object Generator]"),
636                    ObjKind::RegExpObj(r) => {
637                        let d = r.borrow();
638                        format!("/{}/{}", d.re.source, d.re.flags)
639                    }
640                    // `Proxy` を文字列化すると、`ObjKind::Proxy` 専用のケースが無く
641                    // 汎用の `[object Object]` に落ちるバグだった(`String(new
642                    // Proxy([1,2,3], {}))` が `"1,2,3"` になるべきところ壊れた結果に
643                    // なっていた)。`Array.isArray`/`Object.keys`/`structuredClone`
644                    // 等、他の多くの操作は既に target へ委譲する透過性修正済みだった
645                    // のに、この経路だけ見落とされていた。
646                    ObjKind::Proxy { target, .. } => {
647                        Value::Object(target.clone()).to_js_string_seen(seen)
648                    }
649                    // 文字列変換(テンプレートリテラル補間・`String(date)`・暗黙の
650                    // 文字列連結等)でも ISO 形式を返す。この簡易実装ではロケール依存の
651                    // `toDateString()` 的フォーマットは持たないため常に ISO 形式。
652                    ObjKind::DateObj(ms) => date_to_iso_string_repr(*ms),
653                    _ => String::from("[object Object]"),
654                }
655            }
656        }
657    }
658
659    /// `===` 厳密等価。
660    pub fn strict_eq(&self, other: &Value) -> bool {
661        match (self, other) {
662            (Value::Undefined, Value::Undefined) => true,
663            (Value::Null, Value::Null) => true,
664            (Value::Bool(a), Value::Bool(b)) => a == b,
665            (Value::Number(a), Value::Number(b)) => a == b,
666            (Value::Str(a), Value::Str(b)) => a == b,
667            (Value::BigInt(a), Value::BigInt(b)) => a.cmp(b) == core::cmp::Ordering::Equal,
668            // DOM 要素は `Obj::dom(idx)` がアクセスの都度フレッシュな `Rc` を作るため
669            // (`document.getElementById`/`querySelector`/`.content` 等の戻り値がそう)、
670            // `Rc::ptr_eq` だけでは同じノードを指す2つのハンドルが常に不等になる
671            // バグだった(実ブラウザでは `getElementById('x') === getElementById('x')`
672            // は当然 `true`)。`ObjKind::DomElement` 同士は index で比較する。
673            (Value::Object(a), Value::Object(b)) => {
674                match (&a.borrow().kind, &b.borrow().kind) {
675                    (ObjKind::DomElement(ia), ObjKind::DomElement(ib)) => ia == ib,
676                    _ => Rc::ptr_eq(a, b),
677                }
678            }
679            _ => false,
680        }
681    }
682
683    /// 同一オブジェクト参照か(関数リスナの照合用)。プリミティブは strict_eq に委譲。
684    pub fn same_ref(&self, other: &Value) -> bool {
685        match (self, other) {
686            (Value::Object(a), Value::Object(b)) => Rc::ptr_eq(a, b),
687            _ => self.strict_eq(other),
688        }
689    }
690}
691
692/// JS の数値表示規則(整数は小数点を付けない、NaN/Infinity 対応)。
693pub fn fmt_number(n: f64) -> String {
694    if n.is_nan() {
695        return String::from("NaN");
696    }
697    if n.is_infinite() {
698        return String::from(if n > 0.0 { "Infinity" } else { "-Infinity" });
699    }
700    if n == 0.0 {
701        return String::from("0");
702    }
703    // 整数値は小数点なしで表示。
704    if libm::trunc(n) == n && libm::fabs(n) < 1e21 {
705        return format!("{}", n as i64);
706    }
707    // それ以外は既定の浮動小数表示。
708    let mut s = format!("{}", n);
709    // 末尾の不要な 0 を整理(Rust の {} は概ね妥当だがフォールバック)。
710    if s.contains('.') {
711        while s.ends_with('0') {
712            s.pop();
713        }
714        if s.ends_with('.') {
715            s.pop();
716        }
717    }
718    s
719}
720
721/// `Date` の文字列表現(ISO 8601、UTC)。`builtins.rs` の `format_iso`/`decompose_ms` と
722/// 同じ Howard Hinnant 方式の変換ロジックだが、`value.rs` は `builtins.rs` に依存できない
723/// (下位レイヤのため)ので、ごく小さい純粋関数として独立に複製する
724/// (`kernel::timer` にもある3つ目の複製。詳細は spec/walkthrough.md 2026-07-07 参照)。
725fn date_to_iso_string_repr(ms: f64) -> String {
726    let total_ms = libm::floor(ms) as i64;
727    let days = total_ms.div_euclid(86_400_000);
728    let ms_of_day = total_ms.rem_euclid(86_400_000);
729    let z = days + 719468;
730    let era = (if z >= 0 { z } else { z - 146096 }) / 146097;
731    let doe = z - era * 146097;
732    let yoe = (doe - doe / 1460 + doe / 36524 - doe / 146096) / 365;
733    let y = yoe + era * 400;
734    let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
735    let mp = (5 * doy + 2) / 153;
736    let d = (doy - (153 * mp + 2) / 5 + 1) as u32;
737    let m = (if mp < 10 { mp + 3 } else { mp - 9 }) as u32;
738    let y = if m <= 2 { y + 1 } else { y };
739    let hh = (ms_of_day / 3_600_000) as u32;
740    let mm = ((ms_of_day / 60_000) % 60) as u32;
741    let ss = ((ms_of_day / 1000) % 60) as u32;
742    let mms = (ms_of_day % 1000) as u32;
743    format!(
744        "{:04}-{:02}-{:02}T{:02}:{:02}:{:02}.{:03}Z",
745        y, m, d, hh, mm, ss, mms
746    )
747}