///|
pub fn is_truthy(val : Value) -> Bool {
match val {
Number(n) => n != 0.0 && !n.is_nan()
String_(s) => s.length() > 0
Bool(b) => b
Null => false
Undefined => false
Object(_) => true
Array(_) => true
Symbol(_) => true // Symbols are always truthy
Map(_) => true // Maps are always truthy
Set(_) => true // Sets are always truthy
Promise(_) => true // Promises are always truthy
Proxy(_) => true // Proxies are always truthy
}
}
///|
/// Returns true for Value variants that are JS object types (not primitives).
/// Used to detect when ToPrimitive returns a non-primitive, which is a TypeError.
/// Update here if new object-like Value variants are added.
fn is_js_object(val : Value) -> Bool {
match val {
Object(_) | Array(_) | Map(_) | Set(_) | Promise(_) | Proxy(_) => true
_ => false
}
}
///|
fn resolve_effective_interp(interp : Interpreter?) -> Interpreter? {
interp
}
///|
/// Call a callable value, dispatching to the interpreter for UserFunc types.
/// Supports all callable types including user-defined functions.
/// Pass `interp` explicitly when user-code dispatch may be required.
pub fn call_callable_direct(
callable_val : Value,
this_val : Value,
args : Array[Value],
interp? : Interpreter? = None,
) -> Value raise Error {
match resolve_effective_interp(interp) {
Some(ip) =>
ip.call_value(callable_val, this_val, args, @token.Loc::default())
None =>
match callable_val {
Object(obj_data) =>
match obj_data.callable {
Some(MethodCallable(_, f)) => f(this_val, args)
Some(NativeCallable(_, f)) => f(args)
Some(NativeCallableWithContext(_, f)) => f(Call, args)
Some(NonConstructableCallable(_, f)) => f(args)
Some(UserFunc(_))
| Some(ArrowFunc(_))
| Some(UserFuncExt(_))
| Some(ArrowFuncExt(_))
| Some(BoundFunc(_, _, _))
| Some(InterpreterCallable(_, _))
| Some(InterpreterCallableWithContext(_, _))
| Some(ExecutorCallable(_))
| Some(NonConstructableInterpreterCallable(_, _))
| Some(ClassConstructor(_)) =>
raise @errors.TypeError(
message="Cannot call user function without interpreter context",
)
_ => raise @errors.TypeError(message="Value is not callable")
}
_ => raise @errors.TypeError(message="Value is not callable")
}
}
}
///|
/// SetFunctionName: set name property on anonymous functions per ES2015+.
/// Only sets name if the current name is empty (anonymous).
pub fn set_function_name(val : Value, name : String) -> Unit {
match val {
Object(data) if data.callable is Some(_) =>
// Only set if current name is empty (anonymous)
match data.bag.properties.get("name") {
Some(String_(s)) if s != "" => ()
Some(String_(_)) | None => data.bag.properties["name"] = String_(name)
_ => ()
}
_ => ()
}
}
///|
/// Check if a value is callable
pub fn is_callable(val : Value) -> Bool {
match val {
Object(obj_data) => obj_data.callable is Some(_)
Proxy(proxy_data) => proxy_data.is_callable
_ => false
}
}
///|
/// Look up a string-keyed property on an object, walking the prototype chain.
/// HasProperty: check if a named property exists on the object or its prototype chain
pub fn has_property(
val : Value,
name : String,
interp? : Interpreter? = None,
) -> Bool {
match val {
Object(data) => {
// Pure slot-presence check per ES [[HasProperty]] — must NOT invoke getters.
// Route through has_object_property (uses .contains(), no getter calls) when
// an interpreter is available; fall back to own-slot only when there is none.
let effective = resolve_effective_interp(interp)
match effective {
Some(ip) =>
ip.has_object_property(data, String_(name)) catch {
_ => false
}
None =>
data.bag.properties.contains(name) ||
data.bag.descriptors.contains(name)
}
}
Array(data) => {
if name == "length" {
return true
}
try {
let n = @string.parse_double(name)
let i = n.to_int()
if i.to_double() == n && i >= 0 && i < data.elements.length() {
return true
}
} catch {
_ => ()
}
if get_array_named_prop(data, name) is Some(_) {
return true
}
// Walk Array.prototype chain for inherited properties
let effective = resolve_effective_interp(interp)
match effective {
Some(ip) => {
let ctor = ip.global.get("Array") catch { _ => return false }
match ctor {
Object(ctor_data) =>
match ctor_data.bag.properties.get("prototype") {
Some(Object(proto_data)) =>
ip.has_object_property(proto_data, String_(name)) catch {
_ => false
}
_ => false
}
_ => false
}
}
None => false
}
}
_ => false
}
}
///|
/// Invoke bag.descriptors[name].getter with obj_val as receiver, if one exists.
/// Returns Some(result) if an accessor getter was found and invoked (or if no
/// interpreter is available, Some(Undefined) signals property existence).
/// Returns None if no accessor descriptor with a getter exists for name.
fn invoke_accessor_getter(
bag : PropertyBag,
name : String,
obj_val : Value,
effective : Interpreter?,
) -> Value? raise Error {
match bag.descriptors.get(name) {
Some(desc) =>
match desc.getter {
Some(getter_fn) =>
match effective {
Some(_) =>
Some(
call_callable_direct(getter_fn, obj_val, [], interp=effective),
)
None => Some(Undefined)
}
None => None
}
None => None
}
}
///|
/// Look up a string-keyed property on an object, walking the prototype chain.
/// Handles both data properties (bag.properties) and accessor properties
/// (bag.descriptors with getter), invoking getters with obj_val as receiver.
fn lookup_property_chain(
obj_val : Value,
data : ObjectData,
name : String,
interp? : Interpreter? = None,
) -> Value? raise Error {
let effective = resolve_effective_interp(interp)
// Descriptor check first: accessors write Undefined into bag.properties as a
// sentinel (see apply_descriptor_to_bag / eval_expr ObjectLit Get/Set arms),
// so checking bag.properties first would return that sentinel and skip the
// getter. Mirror get_property_of_object: descriptor → data.
match invoke_accessor_getter(data.bag, name, obj_val, effective) {
Some(v) => return Some(v)
None => ()
}
match data.bag.properties.get(name) {
Some(v) => return Some(v)
None => ()
}
// Walk prototype chain
let mut current = data.prototype
let mut func_fallback_used = false
while true {
match current {
Object(proto_data) => {
match invoke_accessor_getter(proto_data.bag, name, obj_val, effective) {
Some(v) => return Some(v)
None => ()
}
match proto_data.bag.properties.get(name) {
Some(v) => return Some(v)
None => ()
}
current = proto_data.prototype
}
Null =>
// For function objects, fall back to Function.prototype once
if !func_fallback_used {
match data.callable {
Some(_) =>
match effective {
Some(ip) => {
let fp = ip.global.get("[[FunctionPrototype]]") catch {
_ => break
}
func_fallback_used = true
current = fp
}
None => break
}
None => break
}
} else {
break
}
_ => break
}
}
None
}
///|
/// Look up a symbol-keyed property on an object, walking the prototype chain.
/// Also handles getter descriptors - invokes them and returns the result.
pub fn lookup_symbol_property_chain(
obj_val : Value,
data : ObjectData,
sym_id : Int,
interp? : Interpreter? = None,
) -> Value? raise Error {
// Check own symbol descriptors for getter first
match data.bag.symbol_descriptors.get(sym_id) {
Some(desc) =>
match desc.getter {
Some(getter_fn) => {
let result = call_callable_direct(getter_fn, obj_val, [], interp~)
return Some(result)
}
None => ()
}
None => ()
}
// Check own symbol properties
match data.bag.symbol_properties.get(sym_id) {
Some(v) => return Some(v)
None => ()
}
// Walk prototype chain
let mut current = data.prototype
while true {
match current {
Object(proto_data) => {
// Check symbol descriptors for getter
match proto_data.bag.symbol_descriptors.get(sym_id) {
Some(desc) =>
match desc.getter {
Some(getter_fn) => {
let result = call_callable_direct(
getter_fn,
obj_val,
[],
interp~,
)
return Some(result)
}
None => ()
}
None => ()
}
match proto_data.bag.symbol_properties.get(sym_id) {
Some(v) => return Some(v)
None => current = proto_data.prototype
}
}
_ => break
}
}
None
}
///|
/// ES §7.1.1 ToPrimitive step 1: look up @@toPrimitive on the object and call
/// it with the given hint. Handles GetMethod semantics (undefined/null → fall
/// through) and validates that the result is not an Object.
///
/// Returns Some(primitive) if @@toPrimitive was found and produced a primitive.
/// Returns None if @@toPrimitive is absent — caller proceeds to OrdinaryToPrimitive.
/// Raises TypeError if @@toPrimitive is not callable or returns an object.
fn call_symbol_to_primitive(
obj_val : Value,
hint : String,
interp? : Interpreter? = None,
) -> Value? raise Error {
let effective = resolve_effective_interp(interp)
let to_prim_sym = match effective {
Some(ip) => ip.realm_state.well_known_symbols.to_primitive
None => return None
}
let method_value = match effective {
Some(ip) =>
Some(
ip.get_property_key_with_receiver(
obj_val,
Symbol(to_prim_sym),
obj_val,
@token.Loc::default(),
),
)
None =>
match obj_val {
Object(data) =>
lookup_symbol_property_chain(
obj_val,
data,
to_prim_sym.id,
interp=effective,
)
_ => None
}
}
match method_value {
Some(Undefined) | Some(Null) | None => None
Some(exotic_to_prim) => {
if !is_callable(exotic_to_prim) {
raise @errors.TypeError(message="Symbol.toPrimitive is not a function")
}
let result = call_callable_direct(
exotic_to_prim,
obj_val,
[String_(hint)],
interp=effective,
)
if is_js_object(result) {
raise @errors.TypeError(
message="Cannot convert object to primitive value",
)
}
Some(result)
}
}
}
///|
/// ToPrimitive(input, hint "number") - converts an object to a primitive value.
/// Follows the ECMAScript spec: check @@toPrimitive, then valueOf, then toString.
pub fn to_primitive_number(
obj_val : Value,
data : ObjectData,
interp? : Interpreter? = None,
) -> Value raise Error {
match call_symbol_to_primitive(obj_val, "number", interp~) {
Some(result) => return result
None => ()
}
ordinary_to_primitive_number(obj_val, data, interp~)
}
///|
/// Locate a named method on an object for OrdinaryToPrimitive.
/// Mirrors ES Get(O, name):
/// 1. Walk the prototype chain (handles descriptors/getters).
/// 2. If not found there, fall back to the interpreter's get_property
/// (reaches synthetic props that live on the global, e.g. Function.prototype).
/// Chain-path values are returned as-is; callers check is_callable before invoking.
/// Fallback-path values are filtered: only callable results become Some.
fn lookup_ordinary_method(
obj_val : Value,
data : ObjectData,
method_name : String,
interp? : Interpreter? = None,
) -> Value? raise Error {
let effective = resolve_effective_interp(interp)
match lookup_property_chain(obj_val, data, method_name, interp~) {
Some(f) => Some(f)
None =>
match effective {
Some(ip) => {
let v = ip.get_property(obj_val, method_name, @token.Loc::default())
if is_callable(v) {
Some(v)
} else {
None
}
}
None => None
}
}
}
///|
/// OrdinaryToPrimitive(O, "number") - try valueOf first, then toString.
/// Extracted so callers (to_primitive_default, to_primitive_number) can run
/// the ordinary lookup without re-entering the @@toPrimitive check.
fn ordinary_to_primitive_number(
obj_val : Value,
data : ObjectData,
interp? : Interpreter? = None,
) -> Value raise Error {
match lookup_ordinary_method(obj_val, data, "valueOf", interp~) {
Some(vo_fn) =>
if is_callable(vo_fn) {
let result = call_callable_direct(vo_fn, obj_val, [], interp~)
if !is_js_object(result) {
return result
}
}
None => ()
}
match lookup_ordinary_method(obj_val, data, "toString", interp~) {
Some(ts_fn) =>
if is_callable(ts_fn) {
let result = call_callable_direct(ts_fn, obj_val, [], interp~)
if is_js_object(result) {
raise @errors.TypeError(
message="Cannot convert object to primitive value",
)
}
return result
}
None => ()
}
raise @errors.TypeError(message="Cannot convert object to primitive value")
}
///|
fn try_proxy_ordinary_method(
interp : Interpreter,
obj_val : Value,
method_name : String,
) -> Value? raise Error {
let method_val = interp.get_property(
obj_val,
method_name,
@token.Loc::default(),
)
if !is_callable(method_val) {
return None
}
let result = interp.call_value(method_val, obj_val, [], @token.Loc::default())
if is_js_object(result) {
None
} else {
Some(result)
}
}
///|
/// Proxy-aware ToPrimitive shell. Observable property access stays behind the
/// interpreter's canonical [[Get]] dispatcher, while the decision order is a
/// deterministic translation of §7.1.1 / OrdinaryToPrimitive.
fn to_primitive_via_dispatch(
obj_val : Value,
hint : String,
interp? : Interpreter? = None,
) -> Value raise Error {
let effective = match resolve_effective_interp(interp) {
Some(ip) => ip
None =>
return String_(
match obj_val {
Proxy(data) if data.is_callable =>
"function proxy() { [native code] }"
_ => "[object Object]"
},
)
}
let to_prim_sym = effective.realm_state.well_known_symbols.to_primitive
let exotic = effective.get_computed_property(
obj_val,
Symbol(to_prim_sym),
@token.Loc::default(),
)
match exotic {
Undefined | Null => ()
_ => {
if !is_callable(exotic) {
raise @errors.TypeError(message="Symbol.toPrimitive is not a function")
}
let result = effective.call_value(
exotic,
obj_val,
[String_(hint)],
@token.Loc::default(),
)
if is_js_object(result) {
raise @errors.TypeError(
message="Cannot convert object to primitive value",
)
}
return result
}
}
let first = if hint == "string" { "toString" } else { "valueOf" }
let second = if hint == "string" { "valueOf" } else { "toString" }
match try_proxy_ordinary_method(effective, obj_val, first) {
Some(result) => return result
None => ()
}
match try_proxy_ordinary_method(effective, obj_val, second) {
Some(result) => result
None =>
raise @errors.TypeError(
message="Cannot convert object to primitive value",
)
}
}
///|
/// Canonical ToPrimitive entry point for callers that hold an arbitrary
/// ECMAScript value. Primitive inputs pass through; object families that need
/// observable property access use the interpreter dispatchers.
pub fn Interpreter::to_primitive_value(
self : Interpreter,
value : Value,
hint : String,
) -> Value raise Error {
match value {
Object(data) =>
if hint == "string" {
to_primitive_string(value, interp=Some(self))
} else if hint == "number" {
to_primitive_number(value, data, interp=Some(self))
} else {
to_primitive_default(value, data, interp=Some(self))
}
Array(_) | Map(_) | Set(_) | Promise(_) | Proxy(_) =>
to_primitive_via_dispatch(value, hint, interp=Some(self))
_ => value
}
}
///|
/// ToPrimitive(input, hint "default") - for + operator.
/// Same as "number" except passes "default" to @@toPrimitive.
pub fn to_primitive_default(
obj_val : Value,
data : ObjectData,
interp? : Interpreter? = None,
) -> Value raise Error {
match call_symbol_to_primitive(obj_val, "default", interp~) {
Some(result) => return result
None => ()
}
// "default" falls through to the same OrdinaryToPrimitive path as "number".
ordinary_to_primitive_number(obj_val, data, interp~)
}
///|
/// ToPrimitive(input, hint "default") for Array values.
/// Arrays are objects per §7.2.14 steps 10-11 and must be coerced via
/// ToPrimitive before comparison with a primitive operand.
/// Uses the interpreter's symbol/string property lookup so own valueOf and
/// @@toPrimitive hooks are honoured; falls back to to_js_string (join) for
/// plain arrays that have neither hook.
fn to_primitive_default_array(
arr_val : Value,
interp? : Interpreter? = None,
) -> Value raise Error {
let effective = resolve_effective_interp(interp)
match effective {
Some(ip) => {
// Step 1: Check @@toPrimitive via interpreter symbol-keyed lookup.
let to_prim_sym = ip.realm_state.well_known_symbols.to_primitive
let exotic_fn = ip.get_computed_property(
arr_val,
Symbol(to_prim_sym),
@token.Loc::default(),
)
match exotic_fn {
Undefined | Null => ()
_ => {
if !is_callable(exotic_fn) {
raise @errors.TypeError(
message="Symbol.toPrimitive is not a function",
)
}
let result = call_callable_direct(
exotic_fn,
arr_val,
[String_("default")],
interp=effective,
)
if is_js_object(result) {
raise @errors.TypeError(
message="Cannot convert object to primitive value",
)
}
return result
}
}
// Step 2: Try valueOf via interpreter (handles own + prototype chain).
let vo_fn = ip.get_property(arr_val, "valueOf", @token.Loc::default())
if is_callable(vo_fn) {
let result = call_callable_direct(vo_fn, arr_val, [], interp=effective)
if !is_js_object(result) {
return result
}
}
// Step 3: Try toString (may be user-overridden on the array or its prototype).
let ts_fn = ip.get_property(arr_val, "toString", @token.Loc::default())
if is_callable(ts_fn) {
let result = call_callable_direct(ts_fn, arr_val, [], interp=effective)
if is_js_object(result) {
raise @errors.TypeError(
message="Cannot convert object to primitive value",
)
}
return result
}
// Both methods exhausted without a primitive result (§7.1.1.1 step 3).
raise @errors.TypeError(
message="Cannot convert object to primitive value",
)
}
None => ()
}
// interp=None fallback: join elements directly (no context to dispatch JS methods).
String_(to_js_string(arr_val, interp~))
}
///|
pub fn is_es_whitespace_cp(cp : Int) -> Bool {
cp == 0x09 ||
cp == 0x0A ||
cp == 0x0B ||
cp == 0x0C ||
cp == 0x0D ||
cp == 0x20 || // Space
cp == 0x00A0 || // No-Break Space
cp == 0xFEFF || // Zero Width No-Break Space (BOM)
cp == 0x1680 || // OGHAM SPACE MARK
(cp >= 0x2000 && cp <= 0x200A) || // EN QUAD through HAIR SPACE
cp == 0x2028 || // Line Separator
cp == 0x2029 || // Paragraph Separator
cp == 0x202F || // NARROW NO-BREAK SPACE
cp == 0x205F || // MEDIUM MATHEMATICAL SPACE
cp == 0x3000 // IDEOGRAPHIC SPACE
}
///|
fn is_js_whitespace_code(ch : UInt16) -> Bool {
is_es_whitespace_cp(ch.to_int())
}
///|
fn js_trim_whitespace(s : String) -> String {
let len = s.length()
let mut start = 0
let mut end = len
while start < end && is_js_whitespace_code(s[start]) {
start = start + 1
}
while end > start && is_js_whitespace_code(s[end - 1]) {
end = end - 1
}
if start == 0 && end == len {
s
} else {
s[start:end].to_owned()
}
}
///|
fn parse_hex_string(s : String) -> Double {
guard s.length() > 0 else { return 0.0 / 0.0 }
let mut result : Double = 0.0
for ch in s {
let digit = if ch >= '0' && ch <= '9' {
ch.to_int() - '0'.to_int()
} else if ch >= 'a' && ch <= 'f' {
ch.to_int() - 'a'.to_int() + 10
} else if ch >= 'A' && ch <= 'F' {
ch.to_int() - 'A'.to_int() + 10
} else {
return 0.0 / 0.0
}
result = result * 16.0 + digit.to_double()
}
result
}
///|
fn parse_binary_string(s : String) -> Double {
guard s.length() > 0 else { return 0.0 / 0.0 }
let mut result : Double = 0.0
for ch in s {
if ch == '0' {
result = result * 2.0
} else if ch == '1' {
result = result * 2.0 + 1.0
} else {
return 0.0 / 0.0
}
}
result
}
///|
fn parse_octal_string(s : String) -> Double {
guard s.length() > 0 else { return 0.0 / 0.0 }
let mut result : Double = 0.0
for ch in s {
if ch >= '0' && ch <= '7' {
result = result * 8.0 + (ch.to_int() - '0'.to_int()).to_double()
} else {
return 0.0 / 0.0
}
}
result
}
///|
pub fn to_number(
val : Value,
interp? : Interpreter? = None,
) -> Double raise Error {
match val {
Number(n) => n
String_(s) => {
let trimmed = js_trim_whitespace(s)
if trimmed.length() == 0 {
return 0.0
}
if trimmed.length() > 2 {
let prefix = trimmed.view()[:2].to_owned()
if prefix == "0x" || prefix == "0X" {
return parse_hex_string(trimmed.view()[2:].to_owned())
}
if prefix == "0b" || prefix == "0B" {
return parse_binary_string(trimmed.view()[2:].to_owned())
}
if prefix == "0o" || prefix == "0O" {
return parse_octal_string(trimmed.view()[2:].to_owned())
}
}
if trimmed == "Infinity" || trimmed == "+Infinity" {
return 1.0 / 0.0
}
if trimmed == "-Infinity" {
return -1.0 / 0.0
}
// Per spec: numeric separators (_) are only valid in source code literals,
// not in Number() string-to-number conversion
if trimmed.contains("_") {
return 0.0 / 0.0
}
// parse_double accepts non-spec strings like "INFINITY" and "inf"; validate first
let valid = trimmed
.iter()
.fold(init=true, fn(acc, ch) {
acc &&
(
(ch >= '0' && ch <= '9') ||
ch == '.' ||
ch == 'e' ||
ch == 'E' ||
ch == '+' ||
ch == '-'
)
})
if !valid {
return 0.0 / 0.0
}
@string.parse_double(trimmed) catch {
_ => 0.0 / 0.0
}
}
Bool(b) => if b { 1.0 } else { 0.0 }
Null => 0.0
Object(data) => {
let prim = to_primitive_number(Object(data), data, interp~)
to_number(prim, interp~)
}
Array(arr_data) => {
let s = arr_data.elements
.map(fn(v) {
match v {
Undefined | Null => ""
_ => v.to_string()
}
})
.join(",")
if s.length() == 0 {
0.0
} else {
@string.parse_double(s) catch {
_ => 0.0 / 0.0
}
}
}
Proxy(_) =>
to_number(to_primitive_via_dispatch(val, "number", interp~), interp~)
Undefined | Map(_) | Set(_) | Promise(_) => 0.0 / 0.0
Symbol(_) =>
raise @errors.TypeError(
message="Cannot convert a Symbol value to a number",
)
}
}
///|
/// ToPrimitive(input, hint "string") - converts an object to a primitive value.
/// Follows the ECMAScript spec: check @@toPrimitive, then toString, then valueOf.
fn to_primitive_string(
obj_val : Value,
interp? : Interpreter? = None,
) -> Value raise Error {
match call_symbol_to_primitive(obj_val, "string", interp~) {
Some(result) => return result
None => ()
}
// Without an interpreter there is no execution context in which to run
// user code. Preserve the legacy built-in ordinary conversions for exotic
// values; runtime expression paths always provide an interpreter and take
// the full Get/Call route below.
if interp is None {
match obj_val {
Array(data) =>
return String_(
data.elements
.map(v => {
match v {
Undefined | Null => ""
_ => v.to_string()
}
})
.join(","),
)
Map(_) | Set(_) | Promise(_) => return String_("[object Object]")
Proxy(data) =>
return String_(
if data.is_callable {
"function proxy() { [native code] }"
} else {
"[object Object]"
},
)
_ => ()
}
}
// Step 2: OrdinaryToPrimitive with hint "string" - try toString first, then valueOf
let data = match obj_val {
Object(data) => Some(data)
_ => None
}
let lookup_method = fn(method_name : String) -> Value? raise Error {
match interp {
Some(ip) =>
Some(
ip.get_property_key_with_receiver(
obj_val,
String_(method_name),
obj_val,
@token.Loc::default(),
),
)
None =>
match data {
Some(object_data) =>
lookup_ordinary_method(obj_val, object_data, method_name, interp~)
None => None
}
}
}
match lookup_method("toString") {
Some(ts_fn) =>
if is_callable(ts_fn) {
let result = call_callable_direct(ts_fn, obj_val, [], interp~)
if !is_js_object(result) {
return result
}
}
None => ()
}
match lookup_method("valueOf") {
Some(vo_fn) =>
if is_callable(vo_fn) {
let result = call_callable_direct(vo_fn, obj_val, [], interp~)
if is_js_object(result) {
raise @errors.TypeError(
message="Cannot convert object to primitive value",
)
}
return result
}
None => ()
}
raise @errors.TypeError(message="Cannot convert object to primitive value")
}
///|
/// ECMAScript ToPropertyKey §7.1.19 — canonicalize a Value into a property
/// key. Symbols pass through; anything else is ToPrimitive(hint:"string") +
/// ToString via `to_js_string`, which invokes user-land Symbol.toPrimitive
/// / toString hooks (so the caller observes side-effects in spec order).
pub fn to_property_key(
val : Value,
interp? : Interpreter? = None,
) -> Value raise Error {
match val {
Symbol(_) => val
Object(_) | Array(_) | Map(_) | Set(_) | Promise(_) | Proxy(_) => {
let primitive = to_primitive_string(val, interp~)
match primitive {
Symbol(_) => primitive
_ => Value::String_(to_js_string(primitive, interp~))
}
}
_ => Value::String_(to_js_string(val, interp~))
}
}
///|
/// ECMAScript ToString - converts a value to a string following the spec.
/// For objects, calls ToPrimitive(hint: "string") then converts result to string.
pub fn to_js_string(
val : Value,
interp? : Interpreter? = None,
) -> String raise Error {
match val {
String_(s) => s
Number(n) => {
let i = n.to_int()
if i.to_double() == n && !n.is_inf() && !n.is_nan() {
i.to_string()
} else {
n.to_string()
}
}
Bool(b) => b.to_string()
Null => "null"
Undefined => "undefined"
Symbol(_) =>
raise @errors.TypeError(
message="Cannot convert a Symbol value to a string",
)
Object(_) | Array(_) | Map(_) | Set(_) | Promise(_) | Proxy(_) => {
let prim = to_primitive_string(val, interp~)
to_js_string(prim, interp~)
}
}
}
///|
/// Interpreter method: ToNumber with explicit interpreter context.
pub fn Interpreter::to_number(
self : Interpreter,
val : Value,
) -> Double raise Error {
to_number(val, interp=Some(self))
}
///|
/// Interpreter method: ToString with explicit interpreter context.
pub fn Interpreter::to_js_string(
self : Interpreter,
val : Value,
) -> String raise Error {
to_js_string(val, interp=Some(self))
}
///|
/// Interpreter method: HasProperty with explicit interpreter context.
pub fn Interpreter::has_property(
self : Interpreter,
val : Value,
name : String,
) -> Bool raise Error {
self.has_property_key(val, String_(name))
}
///|
fn map_target_prototype(realm_state : RealmState, data : MapData) -> Value {
data.prototype.unwrap_or_else(fn() { realm_state.get_map_proto() })
}
///|
fn set_target_prototype(realm_state : RealmState, data : SetData) -> Value {
data.prototype.unwrap_or_else(fn() { realm_state.get_set_proto() })
}
///|
fn promise_target_prototype(
realm_state : RealmState,
data : PromiseData,
) -> Value {
data.prototype.unwrap_or_else(fn() { realm_state.get_promise_proto() })
}
///|
/// ES §7.3.11 `HasProperty` with a pre-computed property key.
pub fn Interpreter::has_property_key(
self : Interpreter,
val : Value,
key : Value,
) -> Bool raise Error {
match val {
Proxy(proxy_data) => proxy_has_property_key(self, proxy_data, key)
Object(data) => self.has_object_property(data, key)
Array(data) => self.has_array_property(data, key)
// Map/Set/Promise share a uniform shape: an expando bag plus a builtin
// prototype. Implement OrdinaryHasProperty (ES §7.3.11): own bag →
// proto chain → bool, with no value inspection. Resolve from the target
// object's own prototype chain, not the active callee realm.
Map(data) =>
self.has_bag_with_proto(
data.bag,
map_target_prototype(self.realm_state, data),
key,
)
Set(data) =>
self.has_bag_with_proto(
data.bag,
set_target_prototype(self.realm_state, data),
key,
)
Promise(data) =>
self.has_bag_with_proto(
data.bag,
promise_target_prototype(self.realm_state, data),
key,
)
_ => false
}
}
///|
fn Interpreter::has_object_property(
self : Interpreter,
data : ObjectData,
key : Value,
) -> Bool raise Error {
if is_typedarray_class(data.class_name) {
match key {
String_(s) =>
match classify_typedarray_string_key(s) {
Some(-1) => return false
Some(idx) =>
return (self.stdlib_hooks.typedarray_is_valid_index)(
data,
idx,
self.realm_state,
)
None => ()
}
_ => ()
}
}
match key {
Symbol(sym) =>
if data.bag.symbol_properties.contains(sym.id) ||
data.bag.symbol_descriptors.contains(sym.id) {
return true
}
_ => {
let name = to_js_string(key)
if data.bag.properties.contains(name) ||
data.bag.descriptors.contains(name) {
return true
}
}
}
if self.has_property_on_proto(data.prototype, key) {
return true
}
match data.callable {
Some(_) => {
let func_proto = self.global.get("[[FunctionPrototype]]") catch {
_ => return false
}
let func_proto_data = match func_proto {
Object(d) => d
_ => return false
}
// Guard 1: we ARE Function.prototype — walking it again causes infinite recursion.
// Guard 2: we already walked Function.prototype above (data.prototype == func_proto).
if physical_equal(data, func_proto_data) ||
physical_equal(data.prototype, func_proto) {
return false
}
self.has_property_on_proto(func_proto, key)
}
None => false
}
}
///|
fn Interpreter::has_array_property(
self : Interpreter,
data : ArrayData,
key : Value,
) -> Bool raise Error {
match key {
Symbol(sym) =>
if get_array_symbol_prop(data, sym.id) is Some(_) ||
data.bag.symbol_descriptors.contains(sym.id) {
return true
}
_ => {
let name = to_js_string(key)
if name == "length" {
return true
}
let idx = @string.parse_int(name) catch { _ => -1 }
if idx >= 0 && idx.to_string() == name {
// Route through the central index classifier so accessor
// descriptors and the Phase-3 hole / stale-marker semantics
// stay consistent with `array_index_lookup_result` callers.
match array_index_lookup_result(data, idx) {
Present(_) | OwnAccessor(_) => return true
Hole | OutOfRange => ()
}
}
if data.bag.properties.contains(name) ||
data.bag.descriptors.contains(name) {
return true
}
}
}
self.has_property_on_proto(get_array_prototype(self.realm_state, data), key)
}
///|
fn Interpreter::has_property_on_proto(
self : Interpreter,
proto : Value,
key : Value,
) -> Bool raise Error {
match proto {
Null | Undefined => false
Proxy(proxy_data) => proxy_has_property_key(self, proxy_data, key)
Object(proto_data) => self.has_object_property(proto_data, key)
Array(arr_data) => self.has_array_property(arr_data, key)
Map(data) =>
self.has_bag_with_proto(
data.bag,
map_target_prototype(self.realm_state, data),
key,
)
Set(data) =>
self.has_bag_with_proto(
data.bag,
set_target_prototype(self.realm_state, data),
key,
)
Promise(data) =>
self.has_bag_with_proto(
data.bag,
promise_target_prototype(self.realm_state, data),
key,
)
_ => false
}
}
///|
fn bag_has_key(bag : PropertyBag, key : Value) -> Bool raise Error {
match key {
Symbol(sym) =>
bag.symbol_properties.contains(sym.id) ||
bag.symbol_descriptors.contains(sym.id)
_ => {
let name = to_js_string(key)
bag.properties.contains(name) || bag.descriptors.contains(name)
}
}
}
///|
/// OrdinaryHasProperty for exotic values whose prototype is cached on
/// RealmState. Checks own-bag descriptors and properties first, then walks
/// the prototype chain from the caller-supplied builtin prototype value.
fn Interpreter::has_bag_with_proto(
self : Interpreter,
bag : PropertyBag,
proto : Value,
key : Value,
) -> Bool raise Error {
if bag_has_key(bag, key) {
return true
}
self.has_property_on_proto(proto, key)
}
///|
/// Interpreter method: ToPrimitive (default hint) with explicit interpreter context.
pub fn Interpreter::to_primitive_default(
self : Interpreter,
obj_val : Value,
data : ObjectData,
) -> Value raise Error {
to_primitive_default(obj_val, data, interp=Some(self))
}
///|
/// ECMAScript ToIndex: converts a value to a non-negative integer index.
/// Throws RangeError for negative values or values >= 2^53.
/// Returns 0 for undefined.
pub fn to_index(val : Value, interp? : Interpreter? = None) -> Int64 raise {
match val {
Undefined => 0L
_ => {
let n = to_number(val, interp~)
let integer_index = if n.is_nan() || n == 0.0 {
0.0
} else if n.is_inf() {
n
} else {
let sign = if n < 0.0 { -1.0 } else { 1.0 }
n.abs().floor() * sign
}
if integer_index.is_inf() ||
integer_index < 0.0 ||
integer_index >= JS_MAX_SAFE_INTEGER_EXCLUSIVE_DOUBLE {
raise @errors.RangeError(message="Invalid index")
}
integer_index.to_int64()
}
}
}
///|
/// Interpreter method: ToIndex with explicit interpreter context.
pub fn Interpreter::to_index(
self : Interpreter,
val : Value,
) -> Int64 raise Error {
to_index(val, interp=Some(self))
}
///|
pub fn to_int32(n : Double) -> Int {
if n.is_nan() || n.is_inf() || n == 0.0 {
return 0
}
// ECMAScript ToInt32: truncate toward zero, modulo 2^32, map to signed range
let two32 : Double = 4294967296.0
// Truncate toward zero
let abs_n = n.abs()
let sign : Double = if n < 0.0 { -1.0 } else { 1.0 }
let truncated = abs_n.floor() * sign
// Modulo 2^32 (always positive)
let remainder = truncated % two32
let pos_mod = if remainder < 0.0 { remainder + two32 } else { remainder }
// Map to signed 32-bit range
if pos_mod >= 2147483648.0 {
(pos_mod - two32).to_int()
} else {
pos_mod.to_int()
}
}
///|
pub fn type_of(val : Value) -> String {
match val {
Number(_) => "number"
String_(_) => "string"
Bool(_) => "boolean"
Null => "object" // JS quirk
Undefined => "undefined"
Object(data) =>
match data.callable {
Some(_) => "function"
None => "object"
}
Array(_) => "object"
Symbol(_) => "symbol"
Map(_) => "object"
Set(_) => "object"
Promise(_) => "object"
Proxy(proxy_data) =>
if proxy_data.is_callable {
"function"
} else {
"object"
}
}
}