Skip to main content

nodejs/
proxy.rs

1//! `Proxy` — the ECMAScript exotic object (10.5) whose essential internal
2//! methods are redirected to a handler's traps.
3//!
4//! A Proxy is not a shape node-js could fake with a property map: every one of
5//! its internal methods has to be diverted, so it is its own heap variant
6//! (`JsObj::Proxy`) and this module is the single place the diversion happens.
7//! The funnels the rest of the runtime already routes through —
8//! `builtins::get_property` / `set_property` / `has_property` /
9//! `delete_property` / `object_keys`, `host::invoke` / `construct_nt` — each
10//! call into here first; when the handler has no trap for the operation, the
11//! `no_trap` fallback re-runs the SAME funnel against the target, which is what
12//! makes `new Proxy(t, {})` observationally indistinguishable from `t`.
13//!
14//! Not implemented, deliberately, and recorded in BUGS.md rather than faked: the
15//! spec's trap-result *invariant* checks (10.5.x steps that throw when a trap
16//! contradicts a non-configurable/non-extensible target property). node-js
17//! reports the trap's answer as given. Every trap itself is real.
18
19use crate::host::{self, with_host, JsObj};
20use fusevm::Value;
21
22/// `(target, handler)` when `v` is a Proxy — revoked or not.
23pub fn parts(v: &Value) -> Option<(Value, Value)> {
24    with_host(|h| match h.get(v) {
25        Some(JsObj::Proxy {
26            target, handler, ..
27        }) => Some((target.clone(), handler.clone())),
28        _ => None,
29    })
30}
31
32/// Whether `v` is a Proxy whose `[[ProxyHandler]]` is still live.
33fn revoked(v: &Value) -> bool {
34    with_host(|h| matches!(h.get(v), Some(JsObj::Proxy { revoked, .. }) if *revoked))
35}
36
37/// The proxy chain's ultimate non-proxy target — what `Array.isArray`,
38/// `Object.prototype.toString` and `typeof` classify by (10.5.x defer those to
39/// `[[ProxyTarget]]`, and a proxy of a proxy defers again).
40pub fn ultimate_target(v: &Value) -> Option<Value> {
41    let mut cur = parts(v)?.0;
42    for _ in 0..100 {
43        match parts(&cur) {
44            Some((t, _)) => cur = t,
45            None => return Some(cur),
46        }
47    }
48    Some(cur)
49}
50
51/// V8's message for an operation attempted on a revoked proxy.
52fn revoked_err(op: &str) -> String {
53    host::type_error(&format!(
54        "Cannot perform '{op}' on a proxy that has been revoked"
55    ))
56}
57
58/// Resolve trap `name` on `v`'s handler.
59///
60/// `Ok(None)` means "not a proxy, or no such trap" — the caller runs its
61/// ordinary path (against the target, for the no-trap case). A revoked proxy
62/// and a non-callable trap both throw here, before any target work happens.
63/// Whether this proxy's handler installs `name` as a callable trap.
64///
65/// Distinguishes "the trap answered, and this is its answer" from "there is no
66/// trap, so the read forwarded to the target" — the two are indistinguishable
67/// in the returned value, and `ToPrimitive` has to tell them apart: a `get`
68/// trap that hands back a non-callable `toString` refuses the conversion, while
69/// a trapless proxy over a `Map` brands as its target does.
70pub fn has_trap(v: &Value, name: &str) -> bool {
71    matches!(trap(v, name), Ok(Some(_)))
72}
73
74fn trap(v: &Value, name: &str) -> Result<Option<(Value, Value, Value)>, String> {
75    let Some((target, handler)) = parts(v) else {
76        return Ok(None);
77    };
78    if revoked(v) {
79        return Err(revoked_err(name));
80    }
81    let t = crate::builtins::get_property(&handler, name)?;
82    if matches!(t, Value::Undef) || with_host(|h| h.is_null(&t)) {
83        return Ok(None);
84    }
85    if !with_host(|h| host::is_callable(h, &t)) {
86        return Err(host::type_error(&format!(
87            "'{}' returned for property '{name}' of object '#<Object>' is not a function",
88            with_host(|h| h.str_of(&t))
89        )));
90    }
91    Ok(Some((t, target, handler)))
92}
93
94/// The target of a proxy whose handler declines the operation (no trap), or
95/// `None` when `v` is not a proxy at all. Errors on a revoked proxy.
96fn no_trap(v: &Value, op: &str) -> Result<Option<Value>, String> {
97    match parts(v) {
98        None => Ok(None),
99        Some((target, _)) if !revoked(v) => Ok(Some(target)),
100        Some(_) => Err(revoked_err(op)),
101    }
102}
103
104/// An internal property key as the JS value a trap receives: the SYMBOL for a
105/// symbol-keyed property (`@@sym:7`, `@@iterator`), a string otherwise. A trap
106/// that inspects its key argument must see what the script wrote.
107pub fn key_value(k: &str) -> Value {
108    with_host(|h| {
109        if let Some(s) = h.symbol_of_key(k) {
110            return s;
111        }
112        match k.strip_prefix("@@") {
113            Some(name) if host::WELL_KNOWN_SYMBOLS.contains(&name) => h.well_known_symbol(name),
114            _ => h.new_str(k),
115        }
116    })
117}
118
119fn call(t: &Value, handler: &Value, args: Vec<Value>) -> Result<Value, String> {
120    host::invoke(t, args, Some(handler.clone()))
121}
122
123// ── the thirteen traps ───────────────────────────────────────────────────────
124
125/// `[[Get]]`. `Ok(None)` → not a proxy; the caller proceeds normally.
126// ── trap invariants (10.5) ──────────────────────────────────────────────────
127//
128// A proxy may lie about most things, but not about a property the TARGET has
129// pinned. None of these checks existed: a trap could report a different value
130// for a non-configurable non-writable property, hide one from `in` or
131// `ownKeys`, claim a frozen object was extensible, or report a prototype an
132// unextensible target does not have. Every one is what a membrane or a
133// hardened-JS shim relies on to know a frozen thing stays frozen.
134fn invariant(msg: &str) -> String {
135    host::type_error(msg)
136}
137
138pub fn get(v: &Value, key: &str, receiver: &Value) -> Result<Option<Value>, String> {
139    if let Some((t, target, handler)) = trap(v, "get")? {
140        let k = key_value(key);
141        let got = call(&t, &handler, vec![target.clone(), k, receiver.clone()])?;
142        // A non-configurable non-writable data property must be reported as it
143        // is on the target.
144        if let Some((val, writable, configurable, is_accessor)) =
145            crate::builtins::own_prop_facts(&target, key)
146        {
147            if !configurable && !is_accessor && !writable && !with_host(|h| h.strict_eq(&got, &val))
148            {
149                return Err(invariant(&format!(
150                    "'get' on proxy: property '{key}' is a read-only and non-configurable data property on the proxy target but the proxy did not return its actual value"
151                )));
152            }
153        }
154        return Ok(Some(got));
155    }
156    match no_trap(v, "get")? {
157        Some(target) => crate::builtins::get_property_recv(&target, key, receiver).map(Some),
158        None => Ok(None),
159    }
160}
161
162/// `[[Set]]`. `Ok(true)` means the write was handled here.
163pub fn set(v: &Value, key: &str, val: &Value, receiver: &Value) -> Result<bool, String> {
164    if let Some((t, target, handler)) = trap(v, "set")? {
165        let k = key_value(key);
166        let r = call(
167            &t,
168            &handler,
169            vec![target.clone(), k, val.clone(), receiver.clone()],
170        )?;
171        // A FALSISH return means the trap refused the write. That is silent in
172        // sloppy code and a TypeError in strict — the same split an ordinary
173        // refused write has, and `Reflect.set` reports it as `false` either way.
174        // The return value was discarded, so a refusing trap looked like a
175        // successful write.
176        if !with_host(|h| h.truthy(&r)) {
177            return Ok(false);
178        }
179        // Reporting success for a write the target pins is a lie.
180        if let Some((cur, writable, configurable, is_accessor)) =
181            crate::builtins::own_prop_facts(&target, key)
182        {
183            if !configurable && !is_accessor && !writable && !with_host(|h| h.strict_eq(val, &cur))
184            {
185                return Err(invariant(&format!(
186                    "'set' on proxy: trap returned truish for property '{key}' which exists in the proxy target as a non-configurable and non-writable data property with a different value"
187                )));
188            }
189        }
190        return Ok(true);
191    }
192    match no_trap(v, "set")? {
193        Some(target) => {
194            if receiver_observes_set(v, &target, key, receiver) {
195                return define_on_proxy_receiver(v, key, val);
196            }
197            crate::builtins::set_property_pub(&target, key, val.clone())?;
198            Ok(true)
199        }
200        None => Ok(false),
201    }
202}
203
204/// Whether a trapless `[[Set]]` has to run `OrdinarySet` (10.1.9.2) against the
205/// target with THIS proxy as the receiver, rather than forwarding the write.
206///
207/// The forward is observationally identical unless the handler traps one of
208/// the two receiver operations that `OrdinarySet` performs on a data property:
209/// `[[GetOwnProperty]]` (step 3.c) and `[[DefineOwnProperty]]` (3.d.iv / 3.e).
210/// A logging or validating handler that installs only those still sees every
211/// `p.k = v`; forwarding skipped both traps. Only the plain case takes the
212/// receiver route: a data property the target can accept, on an ordinary
213/// target. An accessor or a read-only slot keeps the ordinary forward.
214fn receiver_observes_set(v: &Value, target: &Value, key: &str, receiver: &Value) -> bool {
215    with_host(|h| h.strict_eq(receiver, v))
216        && (has_trap(v, "getOwnPropertyDescriptor") || has_trap(v, "defineProperty"))
217        && parts(target).is_none()
218        && with_host(|h| {
219            host::lookup_accessor(h, target, key).is_none() && h.can_write_prop(target, key)
220        })
221}
222
223/// 10.1.9.2 steps 3.c–3.e with the proxy `v` as the receiver: read its own
224/// descriptor through the `getOwnPropertyDescriptor` trap, then DEFINE through
225/// the `defineProperty` trap — `{ value }` alone over an existing writable data
226/// property, a full enumerable/writable/configurable data descriptor for a new
227/// one. A refused define is a TypeError in strict code, named for the trap that
228/// refused (V8's message), and a silent `false` in sloppy code.
229fn define_on_proxy_receiver(v: &Value, key: &str, val: &Value) -> Result<bool, String> {
230    let existing = get_own_descriptor(v, key)?.unwrap_or(Value::Undef);
231    let mut desc = indexmap::IndexMap::new();
232    desc.insert("value".to_string(), val.clone());
233    if !matches!(existing, Value::Undef) {
234        let field = |n: &str| crate::builtins::get_property(&existing, n);
235        let is_accessor = with_host(|h| {
236            host::lookup_chain(h, &existing, "get").is_some()
237                || host::lookup_chain(h, &existing, "set").is_some()
238        });
239        let writable = field("writable")?;
240        if is_accessor || !with_host(|h| h.truthy(&writable)) {
241            return Ok(false);
242        }
243    } else {
244        for flag in ["writable", "enumerable", "configurable"] {
245            desc.insert(flag.to_string(), Value::Bool(true));
246        }
247    }
248    let desc = with_host(|h| h.new_object(desc));
249    if define_property(v, key, &desc)? {
250        return Ok(true);
251    }
252    if with_host(|h| h.current_strict()) {
253        return Err(host::type_error(&format!(
254            "'defineProperty' on proxy: trap returned falsish for property '{key}'"
255        )));
256    }
257    Ok(false)
258}
259
260/// `[[HasProperty]]` (`key in proxy`).
261pub fn has(v: &Value, key: &str) -> Result<Option<bool>, String> {
262    if let Some((t, target, handler)) = trap(v, "has")? {
263        let k = key_value(key);
264        let r = call(&t, &handler, vec![target.clone(), k])?;
265        let reported = with_host(|h| h.truthy(&r));
266        // A non-configurable property, or any property of a non-extensible
267        // target, cannot be hidden from `in`.
268        if !reported {
269            if let Some((_, _, configurable, _)) = crate::builtins::own_prop_facts(&target, key) {
270                if !configurable || !with_host(|h| h.is_extensible(&target)) {
271                    return Err(invariant(&format!(
272                        "'has' on proxy: trap returned falsish for property '{key}' which exists in the proxy target as non-configurable"
273                    )));
274                }
275            }
276        }
277        return Ok(Some(reported));
278    }
279    match no_trap(v, "has")? {
280        Some(target) => crate::builtins::has_property(&target, key).map(Some),
281        None => Ok(None),
282    }
283}
284
285/// `[[Delete]]`.
286pub fn delete(v: &Value, key: &str) -> Result<Option<bool>, String> {
287    if let Some((t, target, handler)) = trap(v, "deleteProperty")? {
288        let k = key_value(key);
289        let r = call(&t, &handler, vec![target.clone(), k])?;
290        let reported = with_host(|h| h.truthy(&r));
291        // A non-configurable property cannot be reported as deleted.
292        if reported {
293            if let Some((_, _, configurable, _)) = crate::builtins::own_prop_facts(&target, key) {
294                if !configurable {
295                    return Err(invariant(&format!(
296                        "'deleteProperty' on proxy: trap returned truish for property '{key}' which is non-configurable in the proxy target"
297                    )));
298                }
299            }
300        }
301        return Ok(Some(reported));
302    }
303    match no_trap(v, "deleteProperty")? {
304        Some(target) => crate::builtins::delete_property(&target, key).map(Some),
305        None => Ok(None),
306    }
307}
308
309/// `[[OwnPropertyKeys]]`, as INTERNAL key strings (so a symbol key comes back as
310/// `@@sym:<id>` — the form the rest of the runtime indexes by).
311pub fn own_keys(v: &Value) -> Result<Option<Vec<String>>, String> {
312    if let Some((t, target, handler)) = trap(v, "ownKeys")? {
313        let r = call(&t, &handler, vec![target.clone()])?;
314        let items = with_host(|h| h.iter_vec(&r))?;
315        let mut out = Vec::with_capacity(items.len());
316        for k in items {
317            out.push(host::to_property_key(&k)?);
318        }
319        // The list must contain no duplicates …
320        let mut seen: Vec<&String> = Vec::with_capacity(out.len());
321        for k in &out {
322            if seen.contains(&k) {
323                return Err(invariant(&format!(
324                    "'ownKeys' on proxy: trap returned duplicate entries for property '{k}'"
325                )));
326            }
327            seen.push(k);
328        }
329        // … must include every non-configurable own key of the target …
330        let target_keys = with_host(|h| {
331            let mut ks = h.own_key_names(&target, false);
332            ks.extend(
333                h.own_symbol_keys(&target)
334                    .iter()
335                    .map(|sym| h.property_key(sym))
336                    .collect::<Vec<_>>(),
337            );
338            ks
339        });
340        for k in &target_keys {
341            let pinned =
342                crate::builtins::own_prop_facts(&target, k).is_some_and(|(_, _, conf, _)| !conf);
343            if pinned && !out.contains(k) {
344                return Err(invariant(&format!(
345                    "'ownKeys' on proxy: trap result did not include '{k}'"
346                )));
347            }
348        }
349        // … and, for a NON-EXTENSIBLE target, must be exactly its own keys.
350        if !with_host(|h| h.is_extensible(&target)) {
351            for k in &target_keys {
352                if !out.contains(k) {
353                    return Err(invariant(&format!(
354                        "'ownKeys' on proxy: trap result did not include '{k}'"
355                    )));
356                }
357            }
358            for k in &out {
359                if !target_keys.contains(k) {
360                    return Err(invariant(
361                        "'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensible",
362                    ));
363                }
364            }
365        }
366        return Ok(Some(out));
367    }
368    match no_trap(v, "ownKeys")? {
369        Some(target) => {
370            let mut keys = with_host(|h| h.own_key_names(&target, false));
371            keys.extend(with_host(|h| {
372                h.own_symbol_keys(&target)
373                    .iter()
374                    .map(|s| h.property_key(s))
375                    .collect::<Vec<_>>()
376            }));
377            Ok(Some(keys))
378        }
379        None => Ok(None),
380    }
381}
382
383/// `[[GetOwnProperty]]` — the descriptor object (or `undefined`).
384pub fn get_own_descriptor(v: &Value, key: &str) -> Result<Option<Value>, String> {
385    if let Some((t, target, handler)) = trap(v, "getOwnPropertyDescriptor")? {
386        let k = key_value(key);
387        let d = call(&t, &handler, vec![target.clone(), k])?;
388        // A non-configurable property cannot be reported as absent.
389        if matches!(d, Value::Undef) {
390            if let Some((_, _, configurable, _)) = crate::builtins::own_prop_facts(&target, key) {
391                if !configurable {
392                    return Err(invariant(&format!(
393                        "'getOwnPropertyDescriptor' on proxy: trap returned undefined for property '{key}' which is non-configurable in the proxy target"
394                    )));
395                }
396            }
397        }
398        return Ok(Some(d));
399    }
400    match no_trap(v, "getOwnPropertyDescriptor")? {
401        Some(target) => {
402            let k = key_value(key);
403            crate::builtins::own_descriptor_pub(&target, k).map(Some)
404        }
405        None => Ok(None),
406    }
407}
408
409/// `[[DefineOwnProperty]]`.
410pub fn define_property(v: &Value, key: &str, desc: &Value) -> Result<bool, String> {
411    if let Some((t, target, handler)) = trap(v, "defineProperty")? {
412        let k = key_value(key);
413        let r = call(&t, &handler, vec![target.clone(), k, desc.clone()])?;
414        // A FALSISH return means the trap refused. Unlike `set` and
415        // `deleteProperty`, this one throws from `Object.defineProperty` in
416        // SLOPPY code too — only `Reflect.defineProperty` reports it as
417        // `false`. The return value was discarded, so a refusing trap looked
418        // like a successful define.
419        if !with_host(|h| h.truthy(&r)) {
420            return Ok(false);
421        }
422        // A new property cannot be added to a non-extensible target.
423        if crate::builtins::own_prop_facts(&target, key).is_none()
424            && !with_host(|h| h.is_extensible(&target))
425        {
426            return Err(invariant(&format!(
427                "'defineProperty' on proxy: trap returned truish for adding property '{key}' to the non-extensible proxy target"
428            )));
429        }
430        return Ok(true);
431    }
432    match no_trap(v, "defineProperty")? {
433        Some(target) => {
434            let k = key_value(key);
435            crate::builtins::define_property_pub(&target, k, desc.clone())?;
436            Ok(true)
437        }
438        None => Ok(false),
439    }
440}
441
442/// `[[GetPrototypeOf]]`.
443pub fn get_prototype_of(v: &Value) -> Result<Option<Value>, String> {
444    if let Some((t, target, handler)) = trap(v, "getPrototypeOf")? {
445        let reported = call(&t, &handler, vec![target.clone()])?;
446        // A non-extensible target's prototype is fixed, so it must be reported
447        // as it is.
448        if !with_host(|h| h.is_extensible(&target)) {
449            let actual = crate::builtins::prototype_of(&target);
450            if !with_host(|h| h.strict_eq(&reported, &actual)) {
451                return Err(invariant(
452                    "'getPrototypeOf' on proxy: proxy target is non-extensible but the trap did not return its actual prototype",
453                ));
454            }
455        }
456        return Ok(Some(reported));
457    }
458    match no_trap(v, "getPrototypeOf")? {
459        Some(target) => Ok(Some(crate::builtins::prototype_of(&target))),
460        None => Ok(None),
461    }
462}
463
464/// `[[SetPrototypeOf]]`.
465pub fn set_prototype_of(v: &Value, proto: &Value) -> Result<bool, String> {
466    if let Some((t, target, handler)) = trap(v, "setPrototypeOf")? {
467        call(&t, &handler, vec![target, proto.clone()])?;
468        return Ok(true);
469    }
470    match no_trap(v, "setPrototypeOf")? {
471        Some(target) => {
472            with_host(|h| h.set_proto(&target, proto.clone()));
473            Ok(true)
474        }
475        None => Ok(false),
476    }
477}
478
479/// `[[IsExtensible]]`.
480pub fn is_extensible(v: &Value) -> Result<Option<bool>, String> {
481    if let Some((t, target, handler)) = trap(v, "isExtensible")? {
482        // Extensibility cannot be misreported: the answer must match the
483        // target's, so a frozen target cannot be passed off as open.
484        let reported = call(&t, &handler, vec![target.clone()])?;
485        let reported = with_host(|h| h.truthy(&reported));
486        if reported != with_host(|h| h.is_extensible(&target)) {
487            return Err(invariant(
488                "'isExtensible' on proxy: trap result does not reflect extensibility of proxy target",
489            ));
490        }
491        return Ok(Some(reported));
492    }
493    match no_trap(v, "isExtensible")? {
494        Some(target) => Ok(Some(with_host(|h| h.is_extensible(&target)))),
495        None => Ok(None),
496    }
497}
498
499/// `[[PreventExtensions]]`.
500pub fn prevent_extensions(v: &Value) -> Result<bool, String> {
501    if let Some((t, target, handler)) = trap(v, "preventExtensions")? {
502        call(&t, &handler, vec![target])?;
503        return Ok(true);
504    }
505    match no_trap(v, "preventExtensions")? {
506        Some(target) => {
507            with_host(|h| h.prevent_extensions(&target));
508            Ok(true)
509        }
510        None => Ok(false),
511    }
512}
513
514/// `[[Call]]`.
515pub fn apply(v: &Value, args: Vec<Value>, this: Option<Value>) -> Result<Option<Value>, String> {
516    if let Some((t, target, handler)) = trap(v, "apply")? {
517        let this_arg = this.unwrap_or(Value::Undef);
518        let list = with_host(|h| h.new_array(args));
519        return call(&t, &handler, vec![target, this_arg, list]).map(Some);
520    }
521    match no_trap(v, "apply")? {
522        Some(target) => host::invoke(&target, args, this).map(Some),
523        None => Ok(None),
524    }
525}
526
527/// `[[Construct]]`.
528pub fn construct(v: &Value, args: Vec<Value>, new_target: &Value) -> Result<Option<Value>, String> {
529    if let Some((t, target, handler)) = trap(v, "construct")? {
530        let list = with_host(|h| h.new_array(args));
531        return call(&t, &handler, vec![target, list, new_target.clone()]).map(Some);
532    }
533    match no_trap(v, "construct")? {
534        Some(target) => host::construct_nt(&target, args, new_target.clone()).map(Some),
535        None => Ok(None),
536    }
537}
538
539// ── enumeration built on the traps ───────────────────────────────────────────
540
541/// The own keys of a proxy that are ENUMERABLE string keys — `Object.keys`,
542/// `for-in`'s own half, object spread and `JSON.stringify` all need this shape.
543/// 10.5.11 defines it as `ownKeys` filtered by each key's `[[GetOwnProperty]]`,
544/// so both traps really do run, in that order.
545pub fn own_enum_string_keys(v: &Value) -> Result<Vec<String>, String> {
546    let Some(keys) = own_keys(v)? else {
547        return Ok(Vec::new());
548    };
549    let mut out = Vec::new();
550    for k in keys {
551        if host::is_symbol_key(&k) {
552            continue;
553        }
554        let Some(d) = get_own_descriptor(v, &k)? else {
555            continue;
556        };
557        let enumerable = with_host(|h| match h.get(&d) {
558            Some(JsObj::Object(p)) => p.get("enumerable").map(|e| h.truthy(e)).unwrap_or(false),
559            _ => false,
560        });
561        if enumerable {
562            out.push(k);
563        }
564    }
565    Ok(out)
566}
567
568/// Is `key` an own ENUMERABLE property of this proxy right now? One
569/// `getOwnPropertyDescriptor` trap call, which is what `for-in` runs per key at
570/// the moment it visits it (14.7.5.10) — `own_enum_string_keys` answers the same
571/// question for every key at once, which is the wrong shape when the body
572/// between two visits can delete a key or flip its enumerability.
573pub fn own_enumerable(v: &Value, key: &str) -> Result<bool, String> {
574    let Some(d) = get_own_descriptor(v, key)? else {
575        return Ok(false);
576    };
577    Ok(with_host(|h| match h.get(&d) {
578        Some(JsObj::Object(p)) => p.get("enumerable").map(|e| h.truthy(e)).unwrap_or(false),
579        _ => false,
580    }))
581}
582
583/// `(key, value)` for every own enumerable string key — spread / `Object.assign`
584/// / `Object.entries` / `JSON.stringify`. Each value is read through the `get`
585/// trap, as the spec's `CreateDataPropertyOrThrow(…, Get(from, key))` requires.
586pub fn own_enum_entries(v: &Value) -> Result<Vec<(String, Value)>, String> {
587    let keys = own_enum_string_keys(v)?;
588    let mut out = Vec::with_capacity(keys.len());
589    for k in keys {
590        let val = get(v, &k, v)?.unwrap_or(Value::Undef);
591        out.push((k, val));
592    }
593    Ok(out)
594}
595
596/// Whether the proxy chain bottoms out in an Array — the shape `IsArray` and
597/// `Array.prototype[Symbol.iterator]` both key off.
598fn wraps_array(v: &Value) -> bool {
599    match ultimate_target(v) {
600        Some(t) => with_host(|h| matches!(h.get(&t), Some(JsObj::Array(_)))),
601        None => false,
602    }
603}
604
605/// `[...proxy]` / `for (… of proxy)`. `Ok(None)` → not a proxy.
606///
607/// Three cases, in the order `GetIterator` reaches them:
608/// a user `Symbol.iterator` read THROUGH the `get` trap; an array target, whose
609/// `Array.prototype[Symbol.iterator]` observably does `Get(O, "length")` then
610/// `Get(O, i)` (so a `get` trap that lies about either is honored); and anything
611/// else (Map/Set/string/generator target), which iterates as the target does.
612pub fn iterate(v: &Value) -> Result<Option<Vec<Value>>, String> {
613    if parts(v).is_none() {
614        return Ok(None);
615    }
616    let array_backed = wraps_array(v);
617    let iter_fn = get(v, "@@iterator", v)?.unwrap_or(Value::Undef);
618    // node-js models `Array.prototype[Symbol.iterator]` as a thunk BOUND to the
619    // array it was read off, where the real method is generic over `this`. Read
620    // through a proxy, that thunk would walk the TARGET and ignore every answer
621    // the `get` trap gave — so an array-backed proxy still holding the default
622    // falls through to the length-driven walk, which is what the generic method
623    // observably does. A user-installed iterator is an ordinary function value
624    // and keeps the fast path.
625    let default_array_iter =
626        array_backed && with_host(|h| matches!(h.get(&iter_fn), Some(JsObj::BoundMethod { .. })));
627    if !default_array_iter && with_host(|h| host::is_callable(h, &iter_fn)) {
628        let iterator = host::invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
629        return host::drain_iterator(&iterator).map(Some);
630    }
631    if array_backed {
632        let len_v = get(v, "length", v)?.unwrap_or(Value::Undef);
633        let len = with_host(|h| h.to_number(&len_v));
634        let len = if len.is_finite() && len > 0.0 {
635            len as usize
636        } else {
637            0
638        };
639        let mut out = Vec::with_capacity(len);
640        for i in 0..len {
641            out.push(get(v, &i.to_string(), v)?.unwrap_or(Value::Undef));
642        }
643        return Ok(Some(out));
644    }
645    let target = no_trap(v, "get")?.expect("checked it is a proxy");
646    host::iter_all(&target).map(Some)
647}
648
649/// The plain value `JSON.stringify` serializes a proxy as. `SerializeJSONArray`
650/// and `SerializeJSONObject` both read every member through `[[Get]]`, so the
651/// snapshot is taken through the traps rather than off the target.
652pub fn json_snapshot(v: &Value) -> Result<Value, String> {
653    if wraps_array(v) {
654        let items = iterate(v)?.unwrap_or_default();
655        return Ok(with_host(|h| h.new_array(items)));
656    }
657    let entries = own_enum_entries(v)?;
658    Ok(with_host(|h| {
659        let mut m = indexmap::IndexMap::new();
660        for (k, val) in entries {
661            m.insert(k, val);
662        }
663        h.new_object(m)
664    }))
665}
666
667// ── construction ─────────────────────────────────────────────────────────────
668
669/// `new Proxy(target, handler)` (10.5.14 `ProxyCreate`).
670pub fn create(args: &[Value]) -> Result<Value, String> {
671    let target = args.first().cloned().unwrap_or(Value::Undef);
672    let handler = args.get(1).cloned().unwrap_or(Value::Undef);
673    let ok = |v: &Value| {
674        with_host(|h| matches!(v, Value::Obj(_)) && !h.is_null(v) && !host::is_primitive(h, v))
675    };
676    if !ok(&target) || !ok(&handler) {
677        return Err(host::type_error(
678            "Cannot create proxy with a non-object as target or handler",
679        ));
680    }
681    Ok(with_host(|h| {
682        h.alloc(JsObj::Proxy {
683            target,
684            handler,
685            revoked: false,
686        })
687    }))
688}
689
690/// `Proxy.revocable(target, handler)` → `{ proxy, revoke }`. The revoker is a
691/// builtin thunk keyed by the proxy's heap index, so calling it twice is the
692/// no-op the spec asks for rather than a second teardown.
693pub fn revocable(args: &[Value]) -> Result<Value, String> {
694    let proxy = create(args)?;
695    let idx = match proxy {
696        Value::Obj(i) => i,
697        _ => unreachable!("create returns a heap object"),
698    };
699    let revoke = with_host(|h| h.alloc(JsObj::Builtin(format!("@@prevoke:{idx}"))));
700    Ok(with_host(|h| {
701        let mut m = indexmap::IndexMap::new();
702        m.insert("proxy".to_string(), proxy);
703        m.insert("revoke".to_string(), revoke);
704        h.new_object(m)
705    }))
706}
707
708/// Run a `@@prevoke:<idx>` thunk: mark the proxy dead so every trap throws.
709///
710/// The target handle is KEPT rather than nulled as 10.5.15 step 5 words it,
711/// because `typeof` is fixed at creation by whether the target was callable and
712/// V8 still answers `'function'` for a revoked proxy of a function. Nothing can
713/// read the target through the proxy anymore — `revoked` is checked before any
714/// trap or fallback runs.
715pub fn revoke(idx: u32) -> Value {
716    with_host(|h| {
717        if let Some(JsObj::Proxy { revoked, .. }) = h.get_mut(&Value::Obj(idx)) {
718            *revoked = true;
719        }
720    });
721    Value::Undef
722}