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            crate::builtins::set_property_pub(&target, key, val.clone())?;
195            Ok(true)
196        }
197        None => Ok(false),
198    }
199}
200
201/// `[[HasProperty]]` (`key in proxy`).
202pub fn has(v: &Value, key: &str) -> Result<Option<bool>, String> {
203    if let Some((t, target, handler)) = trap(v, "has")? {
204        let k = key_value(key);
205        let r = call(&t, &handler, vec![target.clone(), k])?;
206        let reported = with_host(|h| h.truthy(&r));
207        // A non-configurable property, or any property of a non-extensible
208        // target, cannot be hidden from `in`.
209        if !reported {
210            if let Some((_, _, configurable, _)) = crate::builtins::own_prop_facts(&target, key) {
211                if !configurable || !with_host(|h| h.is_extensible(&target)) {
212                    return Err(invariant(&format!(
213                        "'has' on proxy: trap returned falsish for property '{key}' which exists in the proxy target as non-configurable"
214                    )));
215                }
216            }
217        }
218        return Ok(Some(reported));
219    }
220    match no_trap(v, "has")? {
221        Some(target) => crate::builtins::has_property(&target, key).map(Some),
222        None => Ok(None),
223    }
224}
225
226/// `[[Delete]]`.
227pub fn delete(v: &Value, key: &str) -> Result<Option<bool>, String> {
228    if let Some((t, target, handler)) = trap(v, "deleteProperty")? {
229        let k = key_value(key);
230        let r = call(&t, &handler, vec![target.clone(), k])?;
231        let reported = with_host(|h| h.truthy(&r));
232        // A non-configurable property cannot be reported as deleted.
233        if reported {
234            if let Some((_, _, configurable, _)) = crate::builtins::own_prop_facts(&target, key) {
235                if !configurable {
236                    return Err(invariant(&format!(
237                        "'deleteProperty' on proxy: trap returned truish for property '{key}' which is non-configurable in the proxy target"
238                    )));
239                }
240            }
241        }
242        return Ok(Some(reported));
243    }
244    match no_trap(v, "deleteProperty")? {
245        Some(target) => crate::builtins::delete_property(&target, key).map(Some),
246        None => Ok(None),
247    }
248}
249
250/// `[[OwnPropertyKeys]]`, as INTERNAL key strings (so a symbol key comes back as
251/// `@@sym:<id>` — the form the rest of the runtime indexes by).
252pub fn own_keys(v: &Value) -> Result<Option<Vec<String>>, String> {
253    if let Some((t, target, handler)) = trap(v, "ownKeys")? {
254        let r = call(&t, &handler, vec![target.clone()])?;
255        let items = with_host(|h| h.iter_vec(&r))?;
256        let mut out = Vec::with_capacity(items.len());
257        for k in items {
258            out.push(host::to_property_key(&k)?);
259        }
260        // The list must contain no duplicates …
261        let mut seen: Vec<&String> = Vec::with_capacity(out.len());
262        for k in &out {
263            if seen.contains(&k) {
264                return Err(invariant(&format!(
265                    "'ownKeys' on proxy: trap returned duplicate entries for property '{k}'"
266                )));
267            }
268            seen.push(k);
269        }
270        // … must include every non-configurable own key of the target …
271        let target_keys = with_host(|h| {
272            let mut ks = h.own_key_names(&target, false);
273            ks.extend(
274                h.own_symbol_keys(&target)
275                    .iter()
276                    .map(|sym| h.property_key(sym))
277                    .collect::<Vec<_>>(),
278            );
279            ks
280        });
281        for k in &target_keys {
282            let pinned =
283                crate::builtins::own_prop_facts(&target, k).is_some_and(|(_, _, conf, _)| !conf);
284            if pinned && !out.contains(k) {
285                return Err(invariant(&format!(
286                    "'ownKeys' on proxy: trap result did not include '{k}'"
287                )));
288            }
289        }
290        // … and, for a NON-EXTENSIBLE target, must be exactly its own keys.
291        if !with_host(|h| h.is_extensible(&target)) {
292            for k in &target_keys {
293                if !out.contains(k) {
294                    return Err(invariant(&format!(
295                        "'ownKeys' on proxy: trap result did not include '{k}'"
296                    )));
297                }
298            }
299            for k in &out {
300                if !target_keys.contains(k) {
301                    return Err(invariant(
302                        "'ownKeys' on proxy: trap returned extra keys but proxy target is non-extensible",
303                    ));
304                }
305            }
306        }
307        return Ok(Some(out));
308    }
309    match no_trap(v, "ownKeys")? {
310        Some(target) => {
311            let mut keys = with_host(|h| h.own_key_names(&target, false));
312            keys.extend(with_host(|h| {
313                h.own_symbol_keys(&target)
314                    .iter()
315                    .map(|s| h.property_key(s))
316                    .collect::<Vec<_>>()
317            }));
318            Ok(Some(keys))
319        }
320        None => Ok(None),
321    }
322}
323
324/// `[[GetOwnProperty]]` — the descriptor object (or `undefined`).
325pub fn get_own_descriptor(v: &Value, key: &str) -> Result<Option<Value>, String> {
326    if let Some((t, target, handler)) = trap(v, "getOwnPropertyDescriptor")? {
327        let k = key_value(key);
328        let d = call(&t, &handler, vec![target.clone(), k])?;
329        // A non-configurable property cannot be reported as absent.
330        if matches!(d, Value::Undef) {
331            if let Some((_, _, configurable, _)) = crate::builtins::own_prop_facts(&target, key) {
332                if !configurable {
333                    return Err(invariant(&format!(
334                        "'getOwnPropertyDescriptor' on proxy: trap returned undefined for property '{key}' which is non-configurable in the proxy target"
335                    )));
336                }
337            }
338        }
339        return Ok(Some(d));
340    }
341    match no_trap(v, "getOwnPropertyDescriptor")? {
342        Some(target) => {
343            let k = key_value(key);
344            crate::builtins::own_descriptor_pub(&target, k).map(Some)
345        }
346        None => Ok(None),
347    }
348}
349
350/// `[[DefineOwnProperty]]`.
351pub fn define_property(v: &Value, key: &str, desc: &Value) -> Result<bool, String> {
352    if let Some((t, target, handler)) = trap(v, "defineProperty")? {
353        let k = key_value(key);
354        let r = call(&t, &handler, vec![target.clone(), k, desc.clone()])?;
355        // A FALSISH return means the trap refused. Unlike `set` and
356        // `deleteProperty`, this one throws from `Object.defineProperty` in
357        // SLOPPY code too — only `Reflect.defineProperty` reports it as
358        // `false`. The return value was discarded, so a refusing trap looked
359        // like a successful define.
360        if !with_host(|h| h.truthy(&r)) {
361            return Ok(false);
362        }
363        // A new property cannot be added to a non-extensible target.
364        if crate::builtins::own_prop_facts(&target, key).is_none()
365            && !with_host(|h| h.is_extensible(&target))
366        {
367            return Err(invariant(&format!(
368                "'defineProperty' on proxy: trap returned truish for adding property '{key}' to the non-extensible proxy target"
369            )));
370        }
371        return Ok(true);
372    }
373    match no_trap(v, "defineProperty")? {
374        Some(target) => {
375            let k = key_value(key);
376            crate::builtins::define_property_pub(&target, k, desc.clone())?;
377            Ok(true)
378        }
379        None => Ok(false),
380    }
381}
382
383/// `[[GetPrototypeOf]]`.
384pub fn get_prototype_of(v: &Value) -> Result<Option<Value>, String> {
385    if let Some((t, target, handler)) = trap(v, "getPrototypeOf")? {
386        let reported = call(&t, &handler, vec![target.clone()])?;
387        // A non-extensible target's prototype is fixed, so it must be reported
388        // as it is.
389        if !with_host(|h| h.is_extensible(&target)) {
390            let actual = crate::builtins::prototype_of(&target);
391            if !with_host(|h| h.strict_eq(&reported, &actual)) {
392                return Err(invariant(
393                    "'getPrototypeOf' on proxy: proxy target is non-extensible but the trap did not return its actual prototype",
394                ));
395            }
396        }
397        return Ok(Some(reported));
398    }
399    match no_trap(v, "getPrototypeOf")? {
400        Some(target) => Ok(Some(crate::builtins::prototype_of(&target))),
401        None => Ok(None),
402    }
403}
404
405/// `[[SetPrototypeOf]]`.
406pub fn set_prototype_of(v: &Value, proto: &Value) -> Result<bool, String> {
407    if let Some((t, target, handler)) = trap(v, "setPrototypeOf")? {
408        call(&t, &handler, vec![target, proto.clone()])?;
409        return Ok(true);
410    }
411    match no_trap(v, "setPrototypeOf")? {
412        Some(target) => {
413            with_host(|h| h.set_proto(&target, proto.clone()));
414            Ok(true)
415        }
416        None => Ok(false),
417    }
418}
419
420/// `[[IsExtensible]]`.
421pub fn is_extensible(v: &Value) -> Result<Option<bool>, String> {
422    if let Some((t, target, handler)) = trap(v, "isExtensible")? {
423        // Extensibility cannot be misreported: the answer must match the
424        // target's, so a frozen target cannot be passed off as open.
425        let reported = call(&t, &handler, vec![target.clone()])?;
426        let reported = with_host(|h| h.truthy(&reported));
427        if reported != with_host(|h| h.is_extensible(&target)) {
428            return Err(invariant(
429                "'isExtensible' on proxy: trap result does not reflect extensibility of proxy target",
430            ));
431        }
432        return Ok(Some(reported));
433    }
434    match no_trap(v, "isExtensible")? {
435        Some(target) => Ok(Some(with_host(|h| h.is_extensible(&target)))),
436        None => Ok(None),
437    }
438}
439
440/// `[[PreventExtensions]]`.
441pub fn prevent_extensions(v: &Value) -> Result<bool, String> {
442    if let Some((t, target, handler)) = trap(v, "preventExtensions")? {
443        call(&t, &handler, vec![target])?;
444        return Ok(true);
445    }
446    match no_trap(v, "preventExtensions")? {
447        Some(target) => {
448            with_host(|h| h.prevent_extensions(&target));
449            Ok(true)
450        }
451        None => Ok(false),
452    }
453}
454
455/// `[[Call]]`.
456pub fn apply(v: &Value, args: Vec<Value>, this: Option<Value>) -> Result<Option<Value>, String> {
457    if let Some((t, target, handler)) = trap(v, "apply")? {
458        let this_arg = this.unwrap_or(Value::Undef);
459        let list = with_host(|h| h.new_array(args));
460        return call(&t, &handler, vec![target, this_arg, list]).map(Some);
461    }
462    match no_trap(v, "apply")? {
463        Some(target) => host::invoke(&target, args, this).map(Some),
464        None => Ok(None),
465    }
466}
467
468/// `[[Construct]]`.
469pub fn construct(v: &Value, args: Vec<Value>, new_target: &Value) -> Result<Option<Value>, String> {
470    if let Some((t, target, handler)) = trap(v, "construct")? {
471        let list = with_host(|h| h.new_array(args));
472        return call(&t, &handler, vec![target, list, new_target.clone()]).map(Some);
473    }
474    match no_trap(v, "construct")? {
475        Some(target) => host::construct_nt(&target, args, new_target.clone()).map(Some),
476        None => Ok(None),
477    }
478}
479
480// ── enumeration built on the traps ───────────────────────────────────────────
481
482/// The own keys of a proxy that are ENUMERABLE string keys — `Object.keys`,
483/// `for-in`'s own half, object spread and `JSON.stringify` all need this shape.
484/// 10.5.11 defines it as `ownKeys` filtered by each key's `[[GetOwnProperty]]`,
485/// so both traps really do run, in that order.
486pub fn own_enum_string_keys(v: &Value) -> Result<Vec<String>, String> {
487    let Some(keys) = own_keys(v)? else {
488        return Ok(Vec::new());
489    };
490    let mut out = Vec::new();
491    for k in keys {
492        if host::is_symbol_key(&k) {
493            continue;
494        }
495        let Some(d) = get_own_descriptor(v, &k)? else {
496            continue;
497        };
498        let enumerable = with_host(|h| match h.get(&d) {
499            Some(JsObj::Object(p)) => p.get("enumerable").map(|e| h.truthy(e)).unwrap_or(false),
500            _ => false,
501        });
502        if enumerable {
503            out.push(k);
504        }
505    }
506    Ok(out)
507}
508
509/// Is `key` an own ENUMERABLE property of this proxy right now? One
510/// `getOwnPropertyDescriptor` trap call, which is what `for-in` runs per key at
511/// the moment it visits it (14.7.5.10) — `own_enum_string_keys` answers the same
512/// question for every key at once, which is the wrong shape when the body
513/// between two visits can delete a key or flip its enumerability.
514pub fn own_enumerable(v: &Value, key: &str) -> Result<bool, String> {
515    let Some(d) = get_own_descriptor(v, key)? else {
516        return Ok(false);
517    };
518    Ok(with_host(|h| match h.get(&d) {
519        Some(JsObj::Object(p)) => p.get("enumerable").map(|e| h.truthy(e)).unwrap_or(false),
520        _ => false,
521    }))
522}
523
524/// `(key, value)` for every own enumerable string key — spread / `Object.assign`
525/// / `Object.entries` / `JSON.stringify`. Each value is read through the `get`
526/// trap, as the spec's `CreateDataPropertyOrThrow(…, Get(from, key))` requires.
527pub fn own_enum_entries(v: &Value) -> Result<Vec<(String, Value)>, String> {
528    let keys = own_enum_string_keys(v)?;
529    let mut out = Vec::with_capacity(keys.len());
530    for k in keys {
531        let val = get(v, &k, v)?.unwrap_or(Value::Undef);
532        out.push((k, val));
533    }
534    Ok(out)
535}
536
537/// Whether the proxy chain bottoms out in an Array — the shape `IsArray` and
538/// `Array.prototype[Symbol.iterator]` both key off.
539fn wraps_array(v: &Value) -> bool {
540    match ultimate_target(v) {
541        Some(t) => with_host(|h| matches!(h.get(&t), Some(JsObj::Array(_)))),
542        None => false,
543    }
544}
545
546/// `[...proxy]` / `for (… of proxy)`. `Ok(None)` → not a proxy.
547///
548/// Three cases, in the order `GetIterator` reaches them:
549/// a user `Symbol.iterator` read THROUGH the `get` trap; an array target, whose
550/// `Array.prototype[Symbol.iterator]` observably does `Get(O, "length")` then
551/// `Get(O, i)` (so a `get` trap that lies about either is honored); and anything
552/// else (Map/Set/string/generator target), which iterates as the target does.
553pub fn iterate(v: &Value) -> Result<Option<Vec<Value>>, String> {
554    if parts(v).is_none() {
555        return Ok(None);
556    }
557    let array_backed = wraps_array(v);
558    let iter_fn = get(v, "@@iterator", v)?.unwrap_or(Value::Undef);
559    // node-js models `Array.prototype[Symbol.iterator]` as a thunk BOUND to the
560    // array it was read off, where the real method is generic over `this`. Read
561    // through a proxy, that thunk would walk the TARGET and ignore every answer
562    // the `get` trap gave — so an array-backed proxy still holding the default
563    // falls through to the length-driven walk, which is what the generic method
564    // observably does. A user-installed iterator is an ordinary function value
565    // and keeps the fast path.
566    let default_array_iter =
567        array_backed && with_host(|h| matches!(h.get(&iter_fn), Some(JsObj::BoundMethod { .. })));
568    if !default_array_iter && with_host(|h| host::is_callable(h, &iter_fn)) {
569        let iterator = host::invoke(&iter_fn, Vec::new(), Some(v.clone()))?;
570        return host::drain_iterator(&iterator).map(Some);
571    }
572    if array_backed {
573        let len_v = get(v, "length", v)?.unwrap_or(Value::Undef);
574        let len = with_host(|h| h.to_number(&len_v));
575        let len = if len.is_finite() && len > 0.0 {
576            len as usize
577        } else {
578            0
579        };
580        let mut out = Vec::with_capacity(len);
581        for i in 0..len {
582            out.push(get(v, &i.to_string(), v)?.unwrap_or(Value::Undef));
583        }
584        return Ok(Some(out));
585    }
586    let target = no_trap(v, "get")?.expect("checked it is a proxy");
587    host::iter_all(&target).map(Some)
588}
589
590/// The plain value `JSON.stringify` serializes a proxy as. `SerializeJSONArray`
591/// and `SerializeJSONObject` both read every member through `[[Get]]`, so the
592/// snapshot is taken through the traps rather than off the target.
593pub fn json_snapshot(v: &Value) -> Result<Value, String> {
594    if wraps_array(v) {
595        let items = iterate(v)?.unwrap_or_default();
596        return Ok(with_host(|h| h.new_array(items)));
597    }
598    let entries = own_enum_entries(v)?;
599    Ok(with_host(|h| {
600        let mut m = indexmap::IndexMap::new();
601        for (k, val) in entries {
602            m.insert(k, val);
603        }
604        h.new_object(m)
605    }))
606}
607
608// ── construction ─────────────────────────────────────────────────────────────
609
610/// `new Proxy(target, handler)` (10.5.14 `ProxyCreate`).
611pub fn create(args: &[Value]) -> Result<Value, String> {
612    let target = args.first().cloned().unwrap_or(Value::Undef);
613    let handler = args.get(1).cloned().unwrap_or(Value::Undef);
614    let ok = |v: &Value| {
615        with_host(|h| matches!(v, Value::Obj(_)) && !h.is_null(v) && !host::is_primitive(h, v))
616    };
617    if !ok(&target) || !ok(&handler) {
618        return Err(host::type_error(
619            "Cannot create proxy with a non-object as target or handler",
620        ));
621    }
622    Ok(with_host(|h| {
623        h.alloc(JsObj::Proxy {
624            target,
625            handler,
626            revoked: false,
627        })
628    }))
629}
630
631/// `Proxy.revocable(target, handler)` → `{ proxy, revoke }`. The revoker is a
632/// builtin thunk keyed by the proxy's heap index, so calling it twice is the
633/// no-op the spec asks for rather than a second teardown.
634pub fn revocable(args: &[Value]) -> Result<Value, String> {
635    let proxy = create(args)?;
636    let idx = match proxy {
637        Value::Obj(i) => i,
638        _ => unreachable!("create returns a heap object"),
639    };
640    let revoke = with_host(|h| h.alloc(JsObj::Builtin(format!("@@prevoke:{idx}"))));
641    Ok(with_host(|h| {
642        let mut m = indexmap::IndexMap::new();
643        m.insert("proxy".to_string(), proxy);
644        m.insert("revoke".to_string(), revoke);
645        h.new_object(m)
646    }))
647}
648
649/// Run a `@@prevoke:<idx>` thunk: mark the proxy dead so every trap throws.
650///
651/// The target handle is KEPT rather than nulled as 10.5.15 step 5 words it,
652/// because `typeof` is fixed at creation by whether the target was callable and
653/// V8 still answers `'function'` for a revoked proxy of a function. Nothing can
654/// read the target through the proxy anymore — `revoked` is checked before any
655/// trap or fallback runs.
656pub fn revoke(idx: u32) -> Value {
657    with_host(|h| {
658        if let Some(JsObj::Proxy { revoked, .. }) = h.get_mut(&Value::Obj(idx)) {
659            *revoked = true;
660        }
661    });
662    Value::Undef
663}