Files
vectorcraft/crates/js/src/builtins.rs
T
dotCooCoo 16b291aca1 JavaScript: built-ins install under any limits, and class octal escapes match the compiler
A realm made with a low fuel or heap limit lost RegExp and Date, because
installing them compiled a pattern under that limit and the error was
dropped. The built-ins now install before the realm's limits apply.

The early pattern check read an octal class escape starting with 4 to 7
as three digits, while the compiler reads two, so `/[\477-z]/` gave a
SyntaxError for a pattern the compiler accepts. The check now reads
class octal escapes with the compiler's own reader.

docs/target-app-parity.md: the interpreter's entry lists the new
built-ins and the current language-test count.
2026-11-07 21:48:35 -08:00

359 lines
16 KiB
Rust

//! The built-in objects the interpreter itself needs (ECMA-262 5.1 section 15): `Object` and the
//! basic methods of `Object.prototype`, `Function` with `Function.prototype.call`, `apply` and
//! `toString`, `eval`, the error constructors and `Error.prototype.toString`, and the prototypes
//! primitive values look properties up on. `String`, `Number`, `RegExp` and `Date` are in their
//! own modules; the rest of the standard library is not here yet.
use std::rc::Rc;
use crate::bytecode::{CodeKind, Source};
use crate::compiler::{CompileFail, compile_constructed_function};
use crate::heap::{Class, Func, HIDDEN, NativeFn, NativeKind, ObjId, Object, Prop, key, string_bytes};
use crate::interp::{Abrupt, ErrorType, Interp, JsResult};
use crate::value::{Str, Value};
fn alloc(interp: &mut Interp, class: Class, proto: Option<ObjId>) -> ObjId {
interp.heap.alloc_object(Object::new(class, proto))
}
/// A built-in function object.
pub(crate) fn native(interp: &mut Interp, name: &'static str, f: NativeFn, length: u32, kind: NativeKind, constructor: bool) -> JsResult<ObjId> {
let proto = interp.intrinsics.function_proto;
let o = alloc(interp, Class::Function(Func::Native { f, kind, constructor, name }), Some(proto));
interp.define(o, key("length"), Prop::Data { value: Value::Number(f64::from(length)), flags: 0 })?;
Ok(o)
}
pub(crate) fn method(interp: &mut Interp, on: ObjId, name: &'static str, f: NativeFn, length: u32) -> JsResult<()> {
let m = native(interp, name, f, length, NativeKind::Plain, false)?;
interp.define(on, key(name), Prop::Data { value: Value::Object(m), flags: HIDDEN })
}
pub(crate) fn data(interp: &mut Interp, on: ObjId, name: &str, value: Value, flags: u8) -> JsResult<()> {
interp.define(on, key(name), Prop::Data { value, flags })
}
/// Links a constructor and its prototype and makes the constructor a global.
pub(crate) fn constructor(interp: &mut Interp, name: &'static str, ctor: ObjId, proto: ObjId) -> JsResult<()> {
data(interp, ctor, "prototype", Value::Object(proto), 0)?;
data(interp, proto, "constructor", Value::Object(ctor), HIDDEN)?;
let global = interp.global;
data(interp, global, name, Value::Object(ctor), HIDDEN)
}
/// Creates the built-ins and the global object's properties. The caller installs them before it
/// sets the realm's limits, so no limit stops the install.
pub(crate) fn install(interp: &mut Interp) {
if let Err(e) = try_install(interp) {
// Installing touches only objects it has just made, so this does not happen; a realm
// that somehow lacks a built-in reports it when a script uses it.
let _ = e;
}
}
fn try_install(interp: &mut Interp) -> JsResult<()> {
let object_proto = alloc(interp, Class::Ordinary, None);
interp.intrinsics.object_proto = object_proto;
let function_proto = alloc(
interp,
Class::Function(Func::Native { f: function_proto_call, kind: NativeKind::Plain, constructor: false, name: "" }),
Some(object_proto),
);
interp.intrinsics.function_proto = function_proto;
data(interp, function_proto, "length", Value::Number(0.0), 0)?;
let global = interp.global;
if let Some(g) = interp.heap.object_mut(global) {
g.proto = Some(object_proto);
}
interp.intrinsics.array_proto = alloc(interp, Class::Array { length: 0, length_writable: true }, Some(object_proto));
interp.intrinsics.string_proto = alloc(interp, Class::String(Str::from("")), Some(object_proto));
interp.intrinsics.number_proto = alloc(interp, Class::Number(0.0), Some(object_proto));
interp.intrinsics.boolean_proto = alloc(interp, Class::Boolean, Some(object_proto));
interp.intrinsics.regexp_proto = alloc(interp, Class::Ordinary, Some(object_proto));
let thrower = native(interp, "", throw_type_error, 0, NativeKind::Plain, false)?;
if let Some(t) = interp.heap.object_mut(thrower) {
t.extensible = false;
}
interp.intrinsics.throw_type_error = thrower;
data(interp, global, "NaN", Value::Number(f64::NAN), 0)?;
data(interp, global, "Infinity", Value::Number(f64::INFINITY), 0)?;
data(interp, global, "undefined", Value::Undefined, 0)?;
let eval = native(interp, "eval", eval, 1, NativeKind::Eval, false)?;
interp.intrinsics.eval = eval;
data(interp, global, "eval", Value::Object(eval), HIDDEN)?;
let object = native(interp, "Object", object_ctor, 1, NativeKind::Plain, true)?;
constructor(interp, "Object", object, object_proto)?;
method(interp, object_proto, "toString", object_to_string, 0)?;
method(interp, object_proto, "toLocaleString", object_to_locale_string, 0)?;
method(interp, object_proto, "valueOf", object_value_of, 0)?;
method(interp, object_proto, "hasOwnProperty", has_own_property, 1)?;
method(interp, object_proto, "isPrototypeOf", is_prototype_of, 1)?;
method(interp, object_proto, "propertyIsEnumerable", property_is_enumerable, 1)?;
let function = native(interp, "Function", function_ctor, 1, NativeKind::Plain, true)?;
constructor(interp, "Function", function, function_proto)?;
let call = native(interp, "call", unreachable_native, 1, NativeKind::Call, false)?;
data(interp, function_proto, "call", Value::Object(call), HIDDEN)?;
let apply = native(interp, "apply", unreachable_native, 2, NativeKind::Apply, false)?;
data(interp, function_proto, "apply", Value::Object(apply), HIDDEN)?;
method(interp, function_proto, "toString", function_to_string, 0)?;
install_errors(interp, object_proto)?;
crate::string_builtins::install(interp)?;
crate::number_builtins::install(interp)?;
crate::regexp_builtins::install(interp)?;
crate::date_builtins::install(interp)
}
fn install_errors(interp: &mut Interp, object_proto: ObjId) -> JsResult<()> {
let kinds: [(&'static str, NativeFn); 7] = [
("Error", error_ctor),
("EvalError", eval_error_ctor),
("RangeError", range_error_ctor),
("ReferenceError", reference_error_ctor),
("SyntaxError", syntax_error_ctor),
("TypeError", type_error_ctor),
("URIError", uri_error_ctor),
];
let mut error_proto = object_proto;
for (i, (name, f)) in kinds.into_iter().enumerate() {
// Error.prototype and each NativeError prototype are Error objects (sections 15.11.4 and
// 15.11.7.7).
let proto = alloc(interp, Class::Error, Some(if i == 0 { object_proto } else { error_proto }));
if i == 0 {
error_proto = proto;
method(interp, proto, "toString", error_to_string, 0)?;
}
if let Some(slot) = interp.intrinsics.error_protos.get_mut(i) {
*slot = proto;
}
data(interp, proto, "name", Value::str(name), HIDDEN)?;
data(interp, proto, "message", Value::str(""), HIDDEN)?;
let ctor = native(interp, name, f, 1, NativeKind::Plain, true)?;
constructor(interp, name, ctor, proto)?;
}
Ok(())
}
pub(crate) fn arg(args: &[Value], i: usize) -> Value {
args.get(i).cloned().unwrap_or(Value::Undefined)
}
fn unreachable_native(_: &mut Interp, _: Value, _: &[Value], _: bool) -> JsResult<Value> {
Err(Abrupt::Internal("call or apply reached its native body".into()))
}
fn throw_type_error(interp: &mut Interp, _: Value, _: &[Value], _: bool) -> JsResult<Value> {
Err(interp.type_error("'caller', 'callee' and 'arguments' cannot be used on strict mode functions"))
}
fn function_proto_call(_: &mut Interp, _: Value, _: &[Value], _: bool) -> JsResult<Value> {
Ok(Value::Undefined)
}
// ---- Object ----
/// `Object(value)` and `new Object(value)` (sections 15.2.1 and 15.2.2).
fn object_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
let v = arg(args, 0);
if v.is_nullish() {
return Ok(Value::Object(interp.new_object()));
}
interp.to_object(&v).map(Value::Object)
}
/// `Object.prototype.toString` (section 15.2.4.2).
fn object_to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
let class = match &this {
Value::Undefined => "Undefined",
Value::Null => "Null",
_ => {
let o = interp.to_object(&this)?;
interp.heap.object(o).map_or("Object", |o| o.class.name())
}
};
Ok(Value::str(&format!("[object {class}]")))
}
fn object_to_locale_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
let o = interp.to_object(&this)?;
let f = interp.get_from(o, &key("toString"), &this)?;
if !interp.is_callable(&f) {
return Err(interp.type_error("toString is not a function"));
}
interp.call(&f, &this, &[])
}
fn object_value_of(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
interp.to_object(&this).map(Value::Object)
}
fn has_own_property(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
let k = interp.to_key(&arg(args, 0))?;
let o = interp.to_object(&this)?;
Ok(Value::Bool(interp.get_own_property(o, &k)?.is_some()))
}
fn is_prototype_of(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
let Value::Object(mut v) = arg(args, 0) else { return Ok(Value::Bool(false)) };
let o = interp.to_object(&this)?;
while let Some(p) = interp.heap.object(v).and_then(|v| v.proto) {
if p == o {
return Ok(Value::Bool(true));
}
v = p;
interp.use_fuel()?;
}
Ok(Value::Bool(false))
}
fn property_is_enumerable(interp: &mut Interp, this: Value, args: &[Value], _: bool) -> JsResult<Value> {
let k = interp.to_key(&arg(args, 0))?;
let o = interp.to_object(&this)?;
Ok(Value::Bool(interp.get_own_property(o, &k)?.is_some_and(|p| p.enumerable())))
}
// ---- Function ----
/// `Function(p1, …, body)` and `new Function(…)` (section 15.3.2.1): the source is read as
/// `(function anonymous(p1,…\n) {\nbody\n})` and must be exactly that one function.
fn function_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
const HEAD: &str = "(function anonymous(";
const MID: &str = "\n) {\n";
const TAIL: &str = "\n})";
let mut parts = Vec::with_capacity(args.len());
for a in args {
parts.push(interp.to_str(a)?);
}
let body = parts.pop().unwrap_or_else(|| Str::from(""));
// The length is checked against the source limit before any argument is flattened or copied.
let total = parts
.iter()
.map(Str::len)
.chain([parts.len().saturating_sub(1), HEAD.len() + MID.len() + TAIL.len(), body.len()])
.fold(0usize, usize::saturating_add);
interp.check_source_units(total).map_err(|e| interp.compile_error_to_abrupt(e))?;
let mut units = Vec::with_capacity(total);
units.extend(HEAD.encode_utf16());
for (i, p) in parts.iter().enumerate() {
if i > 0 {
units.push(u16::from(b','));
}
units.extend_from_slice(&p.flat());
}
units.extend(MID.encode_utf16());
units.extend_from_slice(&body.flat());
units.extend(TAIL.encode_utf16());
interp.source_units = interp.source_units.saturating_add(units.len());
let remaining = interp.options.max_source_units.saturating_sub(interp.source_units.saturating_sub(units.len()));
let options = crate::Options { strict: false, max_source_units: remaining, ..interp.options.clone() };
let program = match crate::parse_utf16(&units, &options) {
Ok(p) => p,
Err(e) => return Err(interp.throw(ErrorType::Syntax, &e.message)),
};
let source = Rc::new(Source { lines: program.lines.clone(), text: units.into() });
let code = match compile_constructed_function(&program, &source) {
Ok(Some(code)) => code,
Ok(None) => return Err(interp.throw(ErrorType::Syntax, "the parameters or body do not form one function")),
Err(CompileFail::Syntax(e)) => return Err(interp.throw(ErrorType::Syntax, &e.message)),
Err(CompileFail::Internal(m)) => return Err(Abrupt::Internal(m)),
};
interp.charge_code(&code, 0)?;
let env = interp.global_env;
interp.make_closure(code, env).map(Value::Object)
}
/// `Function.prototype.toString` (section 15.3.4.2): a function's source text, or a stand-in
/// for a built-in.
fn function_to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
let func = this.as_object().and_then(|o| interp.heap.object(o)).and_then(|o| match &o.class {
Class::Function(f) => Some(f.clone()),
_ => None,
});
let text = match func {
Some(Func::Closure { code, .. }) => {
let (start, end) = (code.span.0 as usize, code.span.1 as usize);
let units = code.source.text.get(start..end).unwrap_or_default();
let bytes = string_bytes(units.len());
interp.reserve_heap(bytes)?;
interp.heap.bytes = interp.heap.bytes.saturating_add(bytes);
return Ok(Value::String(Str::from_units(units.to_vec())));
}
Some(Func::Native { name, .. }) => format!("function {name}() {{ [native code] }}"),
Some(Func::Host { name, .. }) => format!("function {name}() {{ [native code] }}"),
None => return Err(interp.type_error("Function.prototype.toString was called on a value that is not a function")),
};
Ok(Value::str(&text))
}
/// The global `eval` called indirectly (section 15.1.2.1.1): global code in the global
/// environment.
fn eval(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
let x = arg(args, 0);
let Value::String(s) = &x else { return Ok(x) };
interp.check_source_units(s.len()).map_err(|e| interp.compile_error_to_abrupt(e))?;
let units = s.flat();
let code = interp.compile_source(&units, false, CodeKind::Eval).map_err(|e| interp.compile_error_to_abrupt(e))?;
interp.run_global(code, true)
}
// ---- errors ----
fn make_error(interp: &mut Interp, t: ErrorType, args: &[Value]) -> JsResult<Value> {
let message = arg(args, 0);
let text = if matches!(message, Value::Undefined) { None } else { Some(interp.to_str(&message)?) };
let o = interp.error_object(t, "");
if let Some(text) = text {
interp.define(o, key("message"), Prop::Data { value: Value::String(text), flags: HIDDEN })?;
}
Ok(Value::Object(o))
}
fn error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
make_error(interp, ErrorType::Error, args)
}
fn eval_error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
make_error(interp, ErrorType::Eval, args)
}
fn range_error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
make_error(interp, ErrorType::Range, args)
}
fn reference_error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
make_error(interp, ErrorType::Reference, args)
}
fn syntax_error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
make_error(interp, ErrorType::Syntax, args)
}
fn type_error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
make_error(interp, ErrorType::Type, args)
}
fn uri_error_ctor(interp: &mut Interp, _: Value, args: &[Value], _: bool) -> JsResult<Value> {
make_error(interp, ErrorType::Uri, args)
}
/// `Error.prototype.toString` (section 15.11.4.4).
fn error_to_string(interp: &mut Interp, this: Value, _: &[Value], _: bool) -> JsResult<Value> {
let Value::Object(o) = this else { return Err(interp.type_error("Error.prototype.toString was called on a value that is not an object")) };
let name = interp.get_from(o, &key("name"), &this)?;
let name = if matches!(name, Value::Undefined) { Str::from("Error") } else { interp.to_str(&name)? };
let message = interp.get_from(o, &key("message"), &this)?;
let message = if matches!(message, Value::Undefined) { Str::from("") } else { interp.to_str(&message)? };
if name.len() == 0 {
return Ok(Value::String(message));
}
if message.len() == 0 {
return Ok(Value::String(name));
}
let sep = Str::from(": ");
let head = interp.concat(&name, &sep)?;
interp.concat(&head, &message).map(Value::String)
}