Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 9 additions & 7 deletions crates/perry-codegen/src/expr/logical_collections.rs
Original file line number Diff line number Diff line change
Expand Up @@ -762,16 +762,18 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
}

// -------- "key" in obj --------
// js_object_has_property takes two NaN-boxed doubles and returns
// a NaN-boxed boolean (1.0/0.0 already in our ABI).
// js_in_operator takes two NaN-boxed doubles and returns a NaN-boxed
// boolean (1.0/0.0 already in our ABI). Unlike the bare
// js_object_has_property helper (used internally by Reflect.has / proxy
// traps / `with` / rest-destructuring), the `in`-operator entry point
// first enforces ECMA-262 13.10.1 step 5: a non-Object right operand
// (`"x" in 5`, `... in null`, `... in Symbol()`, …) throws a TypeError.
Expr::In { property, object } => {
let key = lower_expr(ctx, property)?;
let obj = lower_expr(ctx, object)?;
Ok(ctx.block().call(
DOUBLE,
"js_object_has_property",
&[(DOUBLE, &obj), (DOUBLE, &key)],
))
Ok(ctx
.block()
.call(DOUBLE, "js_in_operator", &[(DOUBLE, &obj), (DOUBLE, &key)]))
}
Expr::PrivateBrandCheck {
class_name,
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/runtime_decls/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -364,6 +364,7 @@ pub fn declare_phase_b_strings(module: &mut LlModule) {
module.declare_function("js_map_from_array", I64, &[I64]);
module.declare_function("js_map_from_iterable", I64, &[DOUBLE]);
module.declare_function("js_object_has_property", DOUBLE, &[DOUBLE, DOUBLE]);
module.declare_function("js_in_operator", DOUBLE, &[DOUBLE, DOUBLE]);
module.declare_function("js_private_brand_check", DOUBLE, &[DOUBLE, I32, PTR, I32]);
module.declare_function(
"js_private_guard",
Expand Down
2 changes: 1 addition & 1 deletion crates/perry-runtime/src/object/field_get_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -151,12 +151,12 @@ pub use field_ops::{
};
pub use get_field_by_name::js_object_get_field_by_name;
pub(crate) use get_field_by_name_tail::get_field_by_name_object_tail;
pub use has_property::js_object_has_property;
pub(super) use has_property::native_module_own_field_by_key;
pub(crate) use has_property::{
closure_dynamic_prop_by_key, reified_function_method_name, wide_key_index_lookup,
wide_key_index_note_hit, WIDE_KEY_INDEX_MIN_KEYS,
};
pub use has_property::{js_in_operator, js_object_has_property};
pub(crate) use ic_miss::{
is_array_method_value_name, is_primitive_proto_method, is_timer_handle_method_key,
set_method_value_name,
Expand Down
110 changes: 110 additions & 0 deletions crates/perry-runtime/src/object/field_get_set/has_property.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,116 @@

use super::*;

/// Render a value the way V8 does inside the `in`-operator TypeError message.
/// Only the primitive RHS shapes that reach `throw_in_operator_non_object` need
/// handling: `null`/`undefined` render literally, a Symbol as `Symbol(desc)`,
/// and every other primitive via its natural string coercion. We must special-
/// case Symbols because `js_jsvalue_to_string` on a Symbol itself throws.
unsafe fn describe_in_operand(value: f64) -> String {
let jv = JSValue::from_bits(value.to_bits());
if jv.is_undefined() {
return "undefined".to_string();
}
if jv.is_null() {
return "null".to_string();
}
if crate::symbol::js_is_symbol(value) != 0 {
let desc = crate::symbol::js_symbol_description(value);
let dv = JSValue::from_bits(desc.to_bits());
if dv.is_undefined() {
return "Symbol()".to_string();
}
return format!(
"Symbol({})",
string_header_to_rust(crate::value::js_jsvalue_to_string(desc))
);
}
string_header_to_rust(crate::value::js_jsvalue_to_string(value))
}

/// Materialize a `*mut StringHeader` into an owned Rust `String` (empty on
/// null). Mirrors the inline conversion in `descriptor_helpers.rs`.
unsafe fn string_header_to_rust(s: *mut crate::string::StringHeader) -> String {
if s.is_null() {
return String::new();
}
let len = (*s).byte_len as usize;
let data = (s as *const u8).add(std::mem::size_of::<crate::string::StringHeader>());
let bytes = std::slice::from_raw_parts(data, len);
std::str::from_utf8(bytes).unwrap_or("").to_string()
}

/// Throw `TypeError: Cannot use 'in' operator to search for '<key>' in <rhs>`,
/// the ECMA-262 13.10.1 step-5 rejection when the right operand of `in` is not
/// an Object. Matches V8's wording; test262 negative cases only assert the
/// error type, but the message keeps parity with Node.
#[cold]
fn throw_in_operator_non_object(obj: f64, key: f64) -> ! {
let (key_str, rhs_str) = unsafe { (describe_in_operand(key), describe_in_operand(obj)) };
let msg = format!("Cannot use 'in' operator to search for '{key_str}' in {rhs_str}");
let msg_val = crate::string::js_string_from_bytes(msg.as_ptr(), msg.len() as u32);
let err = crate::error::js_typeerror_new(msg_val);
crate::exception::js_throw(crate::value::js_nanbox_pointer(err as i64))
}
Comment on lines +45 to +56

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🌐 Web query:

Node.js "in" operator TypeError non-object right operand does it call toString on left operand

💡 Result:

The JavaScript "in" operator does not call toString on its left-hand operand to facilitate a search in the right-hand operand; rather, it converts the left-hand operand to a property key using the abstract operation ToPropertyKey [1][2][3]. The "TypeError: Cannot use 'in' operator to search for 'x' in 'y'" (or similar errors involving undefined or null) occurs because the right-hand operand of the in operator must be an object [4][5][6]. If the right-hand side is a primitive (such as string, number, boolean, undefined, or null), JavaScript throws a TypeError [4][5][6]. Regarding the operands: 1. Left-hand operand: The in operator evaluates the left-hand operand using ToPropertyKey, which converts the value to either a string or a symbol [1][2]. This ensures the value can be used as a property key [1][2]. 2. Right-hand operand: This must be an object (or an object-like value that is not null or undefined) [4][5][6]. If you attempt to use the in operator on a primitive type, the engine immediately throws the TypeError mentioned above [4][5][6]. Commonly, developers encounter this error when they inadvertently pass a primitive (like a string, null, or undefined) to the right side of the in operator, often when expecting an object [5][7][8]. To fix this, you should ensure the right-hand side is a valid object before using the in operator, or use alternative methods (such as Array.prototype.includes() or String.prototype.includes()) if your intent was to search for a value inside a collection rather than a property in an object [5][6].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== has_property.rs ==\n'
cat -n crates/perry-runtime/src/object/field_get_set/has_property.rs | sed -n '1,220p'

printf '\n== describe_in_operand references ==\n'
rg -n "describe_in_operand|throw_in_operator_non_object|in operator|Cannot use 'in' operator" crates/perry-runtime/src -S

printf '\n== key-related lowering / operator implementation references ==\n'
rg -n "ToPropertyKey|js_jsvalue_to_string|in_operator|has_property|HasProperty|property key" crates/perry-runtime/src -S

Repository: PerryTS/perry

Length of output: 50369


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== in-operator lowering / call sites ==\n'
rg -n "js_in_operator\\(|in operator|RelationalExpression.*in|\\bin\\b" crates/perry-runtime/src -S \
  | head -n 120

printf '\n== surrounding operator codegen / bytecode lowering ==\n'
rg -n "js_in_operator|throw_in_operator_non_object|in_rhs_is_object|ToPropertyKey|property key" crates/perry-runtime/src \
  -g '!crates/perry-runtime/src/object/field_get_set/has_property.rs' -S \
  | head -n 200

printf '\n== focused file for codegen references if any ==\n'
fd -a ".*(codegen|compiler|bytecode|lower).*" crates/perry-runtime/src | head -n 50

Repository: PerryTS/perry

Length of output: 16877


🌐 Web query:

ECMAScript in operator algorithm ToPropertyKey ToObject evaluation order spec relationalexpression in shiftexpression

💡 Result:

In ECMAScript, the evaluation order of the relational expression involving the in operator is strictly left-to-right [1]. According to the Runtime Semantics for RelationalExpression: RelationalExpression in ShiftExpression, the evaluation steps are as follows [1]: 1. Let lRef be the result of evaluating the left-hand side (RelationalExpression) [1]. 2. Let lVal be? GetValue(lRef) [1]. 3. Let rRef be the result of evaluating the right-hand side (ShiftExpression) [1]. 4. Let rVal be? GetValue(rRef) [1]. 5. If rVal is not an Object, throw a TypeError exception [1]. 6. Return? HasProperty(rVal,? ToPropertyKey(lVal)) [1]. The left-hand operand is evaluated and converted to a value (lVal) before the right-hand operand is evaluated (rVal) [1]. The ToPropertyKey abstract operation is then applied to the result of the left-hand operand (lVal) to produce the property key [1][2]. The right-hand operand (rVal) must be an Object; if it is not, a TypeError is thrown [1]. Finally, the HasProperty abstract operation is performed to determine if the property key exists on the object [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== value/to_string.rs relevant parts ==\n'
cat -n crates/perry-runtime/src/value/to_string.rs | sed -n '1,220p'

printf '\n== js_jsvalue_to_string callers that are explicitly side-effectful or error-prone ==\n'
rg -n "js_jsvalue_to_string\\(|js_jsvalue_to_string_coerce\\(" crates/perry-runtime/src/value crates/perry-runtime/src/object -S \
  | head -n 120

Repository: PerryTS/perry

Length of output: 18250


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== in-operator tests / side-effect coverage ==\n'
rg -n "in operator|js_in_operator|toString|Symbol\\(\\)|Cannot use 'in' operator" crates/perry-runtime/src -S \
  | head -n 160

printf '\n== focused tests in has_property.rs / ic_miss.rs ==\n'
cat -n crates/perry-runtime/src/object/field_get_set/ic_miss.rs | sed -n '480,560p'

Repository: PerryTS/perry

Length of output: 22501


🌐 Web query:

site:tc39.es/ecma262 in operator algorithm ToPropertyKey ToObject right operand primitive before ToPropertyKey

💡 Result:

The ECMAScript specification defines the runtime evaluation of the in operator within the RelationalExpression production. The evaluation algorithm does not perform ToObject on the right operand before ToPropertyKey on the left operand; rather, it typically involves evaluating both sides and then performing property existence checks [1][2]. Specifically, the runtime evaluation for the RelationalExpression lval in rval proceeds as follows: 1. The left operand is evaluated, and the result is retrieved using GetValue. 2. The right operand is evaluated, and the result is retrieved using GetValue. 3. The left operand's value is converted to a property key using the ToPropertyKey abstract operation [3][4]. 4. The right operand's value is checked to ensure it is an Object; if it is not, a TypeError is thrown. 5. Finally, the HasProperty abstract operation is performed on the object (right operand) using the property key (left operand) to determine if the property exists [3][4]. Because the in operator requires the right operand to be an object (or specifically, it throws if it is not), the algorithm enforces this check before attempting to perform property lookups, and it does not implicitly invoke ToObject on the right operand [3][4]. ToPropertyKey is applied to the left operand to ensure it is a valid property key (a String or Symbol) [3][4].

Citations:


🌐 Web query:

ECMAScript 2025 in operator ToPropertyKey ToObject "Cannot use 'in' operator" left operand object side effect

💡 Result:

In ECMAScript 2025 (the 16th edition of the ECMA-262 specification), the relational in operator evaluates its operands by first evaluating the left-hand side (the property name) and then the right-hand side (the object) [1][2][3]. The abstract operations ToPropertyKey and ToObject are invoked during this process [4]. Specifically: 1. Evaluation Order and Side Effects: The left operand is evaluated first [1]. If this evaluation causes a side effect (such as executing a function or getter), that side effect occurs before the right operand is evaluated. If the right operand evaluation then triggers an error, the side effect from the left operand has already occurred. 2. The in Operator Specification: The operator requires the right-hand side to be an object [5][6]. During the runtime evaluation of RelationalExpression: ShiftExpression in ShiftExpression, the specification mandates that the right operand must be converted to an object [4]. If the right operand is a primitive that cannot be converted to an object (such as null or undefined), the engine throws a TypeError [5][7]. 3. The TypeError: The error "Cannot use 'in' operator" (or "invalid 'in' operand") occurs when the right-hand side is not an object [5][7]. Because the specification evaluates the left operand first, any side effects generated by that evaluation are not undone when the subsequent TypeError is thrown by the right-hand side [1]. In summary, the in operator behaves as follows: - Left operand is evaluated [1]. - Right operand is evaluated [1]. - Right operand is checked to ensure it is an object [5][6]. - If the right operand check fails, a TypeError is thrown [5][7]. Any side effects from the earlier evaluation of the left operand are permanent [1].

Citations:


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '\n== object_ops/has_own.rs relevant comments ==\n'
cat -n crates/perry-runtime/src/object/object_ops/has_own.rs | sed -n '1,140p'

printf '\n== proxy/reflect.rs relevant comments ==\n'
cat -n crates/perry-runtime/src/proxy/reflect.rs | sed -n '100,190p'

Repository: PerryTS/perry

Length of output: 12254


🌐 Web query:

"({toString(){console.log('x')}} in 5)" Node V8 toString side effect

💡 Result:

The expression ({toString{console.log('x')}} in 5) will indeed trigger the side effect in the toString method because the 'in' operator implicitly converts its left-hand operand to a property key [1][2]. When the 'in' operator is used, the JavaScript engine performs the following steps: 1. It evaluates the left-hand operand. In this case, it is an object literal with a custom toString method [3][2]. 2. The 'in' operator requires the left-hand operand to be a property key (a string or symbol) [1]. 3. To convert the object to a string, the engine invokes the object's toString method (or Symbol.toPrimitive if present) [3][4]. 4. Consequently, the console.log('x') inside the custom toString method is executed, printing 'x' to the console. In Node.js and other V8-based environments, this behavior is standard and strictly follows the ECMAScript specification for property key conversion [1][2]. The 'in' operator performs this conversion to ensure that the operand can be used as a key to check for the existence of a property on the right-hand object (in this case, the number 5, which is coerced to a wrapper object) [1][2]. Top results: [1][2]

Citations:


🌐 Web query:

V8 in operator toString side effect primitive right operand

💡 Result:

The JavaScript in operator does not trigger a toString side effect on its right-hand operand because it does not attempt to convert the right-hand operand to a string [1][2]. In fact, if the right-hand operand is a primitive, the in operator will throw a TypeError [2][3]. According to the ECMAScript specification—and as implemented in V8—the in operator requires the right-hand side to be an object (or a JSReceiver in V8's internal terminology) [1][2]. Specifically: 1. The in operator evaluates the left-hand operand, which is converted to a name (using the ToPropertyKey abstract operation, which does involve ToString or ToPrimitive for the key) [1]. 2. It then evaluates the right-hand operand. If the result is not an object, the engine throws a TypeError (e.g., "Cannot use 'in' operator to search for...") [1][3]. 3. Because the engine strictly enforces this object requirement, it never performs a conversion operation (such as ToString) on the right-hand operand [1]. Therefore, there is no mechanism for the in operator to trigger a toString side effect on the object being searched, as it does not perform type coercion on that operand [1][2]. Any toString side effects related to the in operator occur only on the left-hand side (the property key), as it must be coerced into a valid property name [1].

Citations:


Avoid coercing object keys in the in TypeError messagedescribe_in_operand(key) can run user toString/Symbol.toPrimitive on ordinary object keys, but ToPropertyKey for key in rhs happens only after the RHS object check. Render object keys with a non-coercing fallback here to avoid side effects on the throw path.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-runtime/src/object/field_get_set/has_property.rs` around lines
45 - 56, The throw_in_operator_non_object helper is coercing the left-hand key
via describe_in_operand(key), which can trigger user-defined conversion side
effects while building the TypeError message. Update this path so the message
formatting for the `in` operator uses a non-coercing fallback for object-like
keys, and keep the change localized to throw_in_operator_non_object and any
helper it uses for rendering the key.


/// Does the right operand of `in` count as an Object (ECMA-262 13.10.1 step 5)?
/// Mirrors every object-like representation `js_object_has_property` already
/// understands, so the throwing guard in `js_in_operator` never rejects a value
/// that the lookup below would have handled:
///
/// * heap `POINTER_TAG` values — plain objects, arrays, functions/closures,
/// proxies, and every handle-band registry id (Headers/Request/streams/…) —
/// **except** Symbols, which are pointer-tagged but are primitives;
/// * INT32-tagged *registered* class refs (a class used as a value is its
/// constructor object — `"prototype" in SomeClass`);
/// * Web Streams handles — raw finite-integer f64 ids in the stream-id band
/// (`"closed" in reader`).
///
/// Everything else (number, boolean, string, BigInt, null, undefined, Symbol,
/// and any unregistered INT32) is a primitive and makes `in` throw. A numeric
/// literal that happens to land inside the stream-id band is treated as
/// object-like here — a deliberately conservative false-negative that avoids
/// ever regressing a real stream handle; test262's primitive-RHS cases use
/// small literals well below that band.
fn in_rhs_is_object(obj: f64) -> bool {
let jv = JSValue::from_bits(obj.to_bits());
if jv.is_pointer() {
return unsafe { crate::symbol::js_is_symbol(obj) } == 0;
}
if crate::object::class_ref_id(obj).is_some() {
return true;
}
let f = f64::from_bits(obj.to_bits());
f.is_finite()
&& f > 0.0
&& f.fract() == 0.0
&& crate::value::addr_class::is_stream_id_band(f as usize)
}

/// The `in` operator: `key in obj`. ECMA-262 13.10.1 (RelationalExpression `in`)
/// step 5 requires the right operand to be an Object, throwing a `TypeError`
/// otherwise. This is the dedicated codegen entry point for the source-level
/// `in` operator; it performs that spec check and then delegates the actual
/// property lookup to `js_object_has_property`.
///
/// The guard lives here rather than in `js_object_has_property` because that
/// helper is also called internally (Reflect.has, proxy traps, `with`
/// environments, rest-destructuring exclusion, descriptor validation) with
/// receivers that are always objects — routing those through the throwing check
/// would be pointless and risks over-throwing on an internal edge. Only the
/// user-visible `in` operator can legitimately be handed a primitive RHS.
///
/// test262: `language/expressions/in/*` primitive-RHS cases (`"x" in 5`,
/// `... in null`, `... in Symbol()`, `... in ""`, `... in true`, `... in 1n`
/// ⇒ TypeError).
#[no_mangle]
pub extern "C" fn js_in_operator(obj: f64, key: f64) -> f64 {
if !in_rhs_is_object(obj) {
throw_in_operator_non_object(obj, key);
}
js_object_has_property(obj, key)
}

/// Check if a property exists in an object by its string key name
/// Returns NaN-boxed true if the property exists, NaN-boxed false otherwise
/// This implements the JavaScript 'in' operator: "key" in obj
Expand Down
Loading