Browse Source

Implement runtime limits for loops (#2857)

pull/2905/head
Haled Odat 2 years ago committed by GitHub
parent
commit
802d796d51
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 35
      boa_cli/src/debug/limits.rs
  2. 7
      boa_cli/src/debug/mod.rs
  3. 21
      boa_engine/src/builtins/array/tests.rs
  4. 48
      boa_engine/src/builtins/bigint/tests.rs
  5. 4
      boa_engine/src/builtins/date/tests.rs
  6. 11
      boa_engine/src/builtins/function/tests.rs
  7. 4
      boa_engine/src/builtins/json/tests.rs
  8. 4
      boa_engine/src/builtins/map/tests.rs
  9. 13
      boa_engine/src/builtins/number/tests.rs
  10. 40
      boa_engine/src/builtins/object/tests.rs
  11. 32
      boa_engine/src/builtins/regexp/tests.rs
  12. 4
      boa_engine/src/builtins/set/tests.rs
  13. 28
      boa_engine/src/builtins/string/tests.rs
  14. 2
      boa_engine/src/bytecompiler/statement/loop.rs
  15. 31
      boa_engine/src/context/mod.rs
  16. 8
      boa_engine/src/environments/tests.rs
  17. 101
      boa_engine/src/error.rs
  18. 4
      boa_engine/src/lib.rs
  19. 4
      boa_engine/src/object/tests.rs
  20. 14
      boa_engine/src/realm.rs
  21. 10
      boa_engine/src/tests/control_flow/loops.rs
  22. 8
      boa_engine/src/tests/control_flow/mod.rs
  23. 16
      boa_engine/src/tests/function.rs
  24. 16
      boa_engine/src/tests/mod.rs
  25. 60
      boa_engine/src/tests/operators.rs
  26. 4
      boa_engine/src/tests/spread.rs
  27. 4
      boa_engine/src/value/tests.rs
  28. 21
      boa_engine/src/vm/call_frame/env_stack.rs
  29. 1
      boa_engine/src/vm/flowgraph/mod.rs
  30. 24
      boa_engine/src/vm/mod.rs
  31. 16
      boa_engine/src/vm/opcode/call/mod.rs
  32. 65
      boa_engine/src/vm/opcode/iteration/loop_ops.rs
  33. 8
      boa_engine/src/vm/opcode/new/mod.rs
  34. 61
      boa_engine/src/vm/runtime_limits.rs
  35. 39
      boa_engine/src/vm/tests.rs
  36. 47
      boa_examples/src/bin/runtime_limits.rs
  37. 19
      docs/boa_object.md

35
boa_cli/src/debug/limits.rs

@ -0,0 +1,35 @@
use boa_engine::{
object::{FunctionObjectBuilder, ObjectInitializer},
property::Attribute,
Context, JsArgs, JsObject, JsResult, JsValue, NativeFunction,
};
fn get_loop(_: &JsValue, _: &[JsValue], context: &mut Context<'_>) -> JsResult<JsValue> {
let max = context.runtime_limits().loop_iteration_limit();
Ok(JsValue::from(max))
}
fn set_loop(_: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult<JsValue> {
let value = args.get_or_undefined(0).to_length(context)?;
context.runtime_limits_mut().set_loop_iteration_limit(value);
Ok(JsValue::undefined())
}
pub(super) fn create_object(context: &mut Context<'_>) -> JsObject {
let get_loop = FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(get_loop))
.name("get loop")
.length(0)
.build();
let set_loop = FunctionObjectBuilder::new(context, NativeFunction::from_fn_ptr(set_loop))
.name("set loop")
.length(1)
.build();
ObjectInitializer::new(context)
.accessor(
"loop",
Some(get_loop),
Some(set_loop),
Attribute::WRITABLE | Attribute::CONFIGURABLE | Attribute::NON_ENUMERABLE,
)
.build()
}

7
boa_cli/src/debug/mod.rs

@ -5,6 +5,7 @@ use boa_engine::{object::ObjectInitializer, property::Attribute, Context, JsObje
mod function; mod function;
mod gc; mod gc;
mod limits;
mod object; mod object;
mod optimizer; mod optimizer;
mod realm; mod realm;
@ -17,6 +18,7 @@ fn create_boa_object(context: &mut Context<'_>) -> JsObject {
let optimizer_module = optimizer::create_object(context); let optimizer_module = optimizer::create_object(context);
let gc_module = gc::create_object(context); let gc_module = gc::create_object(context);
let realm_module = realm::create_object(context); let realm_module = realm::create_object(context);
let limits_module = limits::create_object(context);
ObjectInitializer::new(context) ObjectInitializer::new(context)
.property( .property(
@ -49,6 +51,11 @@ fn create_boa_object(context: &mut Context<'_>) -> JsObject {
realm_module, realm_module,
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE, Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
) )
.property(
"limits",
limits_module,
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
)
.build() .build()
} }

21
boa_engine/src/builtins/array/tests.rs

@ -1,8 +1,5 @@
use super::Array; use super::Array;
use crate::{ use crate::{builtins::Number, run_test_actions, Context, JsNativeErrorKind, JsValue, TestAction};
builtins::{error::ErrorKind, Number},
run_test_actions, Context, JsValue, TestAction,
};
use indoc::indoc; use indoc::indoc;
#[test] #[test]
@ -191,7 +188,7 @@ fn flat_map_not_callable() {
var array = [1,2,3]; var array = [1,2,3];
array.flatMap("not a function"); array.flatMap("not a function");
"#}, "#},
ErrorKind::Type, JsNativeErrorKind::Type,
"flatMap mapper function is not callable", "flatMap mapper function is not callable",
)]); )]);
} }
@ -639,7 +636,7 @@ fn reduce() {
// Empty array // Empty array
TestAction::assert_native_error( TestAction::assert_native_error(
"[].reduce((acc, x) => acc + x);", "[].reduce((acc, x) => acc + x);",
ErrorKind::Type, JsNativeErrorKind::Type,
"Array.prototype.reduce: called on an empty array and with no initial value", "Array.prototype.reduce: called on an empty array and with no initial value",
), ),
// Array with no defined elements // Array with no defined elements
@ -650,7 +647,7 @@ fn reduce() {
delete deleteArr[1]; delete deleteArr[1];
deleteArr.reduce((acc, x) => acc + x); deleteArr.reduce((acc, x) => acc + x);
"#}, "#},
ErrorKind::Type, JsNativeErrorKind::Type,
"Array.prototype.reduce: called on an empty array and with no initial value", "Array.prototype.reduce: called on an empty array and with no initial value",
), ),
// No callback // No callback
@ -659,7 +656,7 @@ fn reduce() {
var someArr = [0, 1]; var someArr = [0, 1];
someArr.reduce(''); someArr.reduce('');
"#}, "#},
ErrorKind::Type, JsNativeErrorKind::Type,
"Array.prototype.reduce: callback function is not callable", "Array.prototype.reduce: callback function is not callable",
), ),
]); ]);
@ -720,7 +717,7 @@ fn reduce_right() {
// Empty array // Empty array
TestAction::assert_native_error( TestAction::assert_native_error(
"[].reduceRight((acc, x) => acc + x);", "[].reduceRight((acc, x) => acc + x);",
ErrorKind::Type, JsNativeErrorKind::Type,
"Array.prototype.reduceRight: called on an empty array and with no initial value", "Array.prototype.reduceRight: called on an empty array and with no initial value",
), ),
// Array with no defined elements // Array with no defined elements
@ -731,7 +728,7 @@ fn reduce_right() {
delete deleteArr[1]; delete deleteArr[1];
deleteArr.reduceRight((acc, x) => acc + x); deleteArr.reduceRight((acc, x) => acc + x);
"#}, "#},
ErrorKind::Type, JsNativeErrorKind::Type,
"Array.prototype.reduceRight: called on an empty array and with no initial value", "Array.prototype.reduceRight: called on an empty array and with no initial value",
), ),
// No callback // No callback
@ -740,7 +737,7 @@ fn reduce_right() {
var otherArr = [0, 1]; var otherArr = [0, 1];
otherArr.reduceRight(""); otherArr.reduceRight("");
"#}, "#},
ErrorKind::Type, JsNativeErrorKind::Type,
"Array.prototype.reduceRight: callback function is not callable", "Array.prototype.reduceRight: callback function is not callable",
), ),
]); ]);
@ -865,7 +862,7 @@ fn array_spread_arrays() {
fn array_spread_non_iterable() { fn array_spread_non_iterable() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"const array2 = [...5];", "const array2 = [...5];",
ErrorKind::Type, JsNativeErrorKind::Type,
"value with type `number` is not iterable", "value with type `number` is not iterable",
)]); )]);
} }

48
boa_engine/src/builtins/bigint/tests.rs

@ -1,4 +1,4 @@
use crate::{builtins::error::ErrorKind, run_test_actions, JsBigInt, TestAction}; use crate::{run_test_actions, JsBigInt, JsNativeErrorKind, TestAction};
#[test] #[test]
fn equality() { fn equality() {
@ -62,17 +62,17 @@ fn bigint_function_throws() {
run_test_actions([ run_test_actions([
TestAction::assert_native_error( TestAction::assert_native_error(
"BigInt(0.1)", "BigInt(0.1)",
ErrorKind::Range, JsNativeErrorKind::Range,
"cannot convert 0.1 to a BigInt", "cannot convert 0.1 to a BigInt",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"BigInt(null)", "BigInt(null)",
ErrorKind::Type, JsNativeErrorKind::Type,
"cannot convert null to a BigInt", "cannot convert null to a BigInt",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"BigInt(undefined)", "BigInt(undefined)",
ErrorKind::Type, JsNativeErrorKind::Type,
"cannot convert undefined to a BigInt", "cannot convert undefined to a BigInt",
), ),
]); ]);
@ -108,29 +108,37 @@ fn operations() {
), ),
TestAction::assert_eq("15000n / 50n", JsBigInt::from(300)), TestAction::assert_eq("15000n / 50n", JsBigInt::from(300)),
TestAction::assert_eq("15001n / 50n", JsBigInt::from(300)), TestAction::assert_eq("15001n / 50n", JsBigInt::from(300)),
TestAction::assert_native_error("1n/0n", ErrorKind::Range, "BigInt division by zero"), TestAction::assert_native_error(
"1n/0n",
JsNativeErrorKind::Range,
"BigInt division by zero",
),
TestAction::assert_eq("15007n % 10n", JsBigInt::from(7)), TestAction::assert_eq("15007n % 10n", JsBigInt::from(7)),
TestAction::assert_native_error("1n % 0n", ErrorKind::Range, "BigInt division by zero"), TestAction::assert_native_error(
"1n % 0n",
JsNativeErrorKind::Range,
"BigInt division by zero",
),
TestAction::assert_eq( TestAction::assert_eq(
"100n ** 10n", "100n ** 10n",
JsBigInt::from_string("100000000000000000000").unwrap(), JsBigInt::from_string("100000000000000000000").unwrap(),
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"10n ** (-10n)", "10n ** (-10n)",
ErrorKind::Range, JsNativeErrorKind::Range,
"BigInt negative exponent", "BigInt negative exponent",
), ),
TestAction::assert_eq("8n << 2n", JsBigInt::from(32)), TestAction::assert_eq("8n << 2n", JsBigInt::from(32)),
TestAction::assert_native_error( TestAction::assert_native_error(
"1000n << 1000000000000000n", "1000n << 1000000000000000n",
ErrorKind::Range, JsNativeErrorKind::Range,
"Maximum BigInt size exceeded", "Maximum BigInt size exceeded",
), ),
TestAction::assert_eq("8n >> 2n", JsBigInt::from(2)), TestAction::assert_eq("8n >> 2n", JsBigInt::from(2)),
// TODO: this should return 0n instead of throwing // TODO: this should return 0n instead of throwing
TestAction::assert_native_error( TestAction::assert_native_error(
"1000n >> 1000000000000000n", "1000n >> 1000000000000000n",
ErrorKind::Range, JsNativeErrorKind::Range,
"Maximum BigInt size exceeded", "Maximum BigInt size exceeded",
), ),
]); ]);
@ -151,17 +159,17 @@ fn to_string_invalid_radix() {
run_test_actions([ run_test_actions([
TestAction::assert_native_error( TestAction::assert_native_error(
"10n.toString(null)", "10n.toString(null)",
ErrorKind::Range, JsNativeErrorKind::Range,
"radix must be an integer at least 2 and no greater than 36", "radix must be an integer at least 2 and no greater than 36",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"10n.toString(-1)", "10n.toString(-1)",
ErrorKind::Range, JsNativeErrorKind::Range,
"radix must be an integer at least 2 and no greater than 36", "radix must be an integer at least 2 and no greater than 36",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"10n.toString(37)", "10n.toString(37)",
ErrorKind::Range, JsNativeErrorKind::Range,
"radix must be an integer at least 2 and no greater than 36", "radix must be an integer at least 2 and no greater than 36",
), ),
]); ]);
@ -219,22 +227,22 @@ fn as_int_n_errors() {
run_test_actions([ run_test_actions([
TestAction::assert_native_error( TestAction::assert_native_error(
"BigInt.asIntN(-1, 0n)", "BigInt.asIntN(-1, 0n)",
ErrorKind::Range, JsNativeErrorKind::Range,
"Index must be between 0 and 2^53 - 1", "Index must be between 0 and 2^53 - 1",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"BigInt.asIntN(-2.5, 0n)", "BigInt.asIntN(-2.5, 0n)",
ErrorKind::Range, JsNativeErrorKind::Range,
"Index must be between 0 and 2^53 - 1", "Index must be between 0 and 2^53 - 1",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"BigInt.asIntN(9007199254740992, 0n)", "BigInt.asIntN(9007199254740992, 0n)",
ErrorKind::Range, JsNativeErrorKind::Range,
"Index must be between 0 and 2^53 - 1", "Index must be between 0 and 2^53 - 1",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"BigInt.asIntN(0n, 0n)", "BigInt.asIntN(0n, 0n)",
ErrorKind::Type, JsNativeErrorKind::Type,
"argument must not be a bigint", "argument must not be a bigint",
), ),
]); ]);
@ -292,22 +300,22 @@ fn as_uint_n_errors() {
run_test_actions([ run_test_actions([
TestAction::assert_native_error( TestAction::assert_native_error(
"BigInt.asUintN(-1, 0n)", "BigInt.asUintN(-1, 0n)",
ErrorKind::Range, JsNativeErrorKind::Range,
"Index must be between 0 and 2^53 - 1", "Index must be between 0 and 2^53 - 1",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"BigInt.asUintN(-2.5, 0n)", "BigInt.asUintN(-2.5, 0n)",
ErrorKind::Range, JsNativeErrorKind::Range,
"Index must be between 0 and 2^53 - 1", "Index must be between 0 and 2^53 - 1",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"BigInt.asUintN(9007199254740992, 0n)", "BigInt.asUintN(9007199254740992, 0n)",
ErrorKind::Range, JsNativeErrorKind::Range,
"Index must be between 0 and 2^53 - 1", "Index must be between 0 and 2^53 - 1",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"BigInt.asUintN(0n, 0n)", "BigInt.asUintN(0n, 0n)",
ErrorKind::Type, JsNativeErrorKind::Type,
"argument must not be a bigint", "argument must not be a bigint",
), ),
]); ]);

4
boa_engine/src/builtins/date/tests.rs

@ -1,4 +1,4 @@
use crate::{builtins::error::ErrorKind, run_test_actions, TestAction}; use crate::{run_test_actions, JsNativeErrorKind, TestAction};
use chrono::{Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone}; use chrono::{Local, NaiveDate, NaiveDateTime, NaiveTime, TimeZone};
use indoc::indoc; use indoc::indoc;
@ -47,7 +47,7 @@ fn timestamp_from_utc(
fn date_this_time_value() { fn date_this_time_value() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"({toString: Date.prototype.toString}).toString()", "({toString: Date.prototype.toString}).toString()",
ErrorKind::Type, JsNativeErrorKind::Type,
"'this' is not a Date", "'this' is not a Date",
)]); )]);
} }

11
boa_engine/src/builtins/function/tests.rs

@ -1,11 +1,10 @@
use crate::{ use crate::{
builtins::error::ErrorKind,
error::JsNativeError, error::JsNativeError,
js_string, js_string,
native_function::NativeFunction, native_function::NativeFunction,
object::{FunctionObjectBuilder, JsObject}, object::{FunctionObjectBuilder, JsObject},
property::{Attribute, PropertyDescriptor}, property::{Attribute, PropertyDescriptor},
run_test_actions, JsValue, TestAction, run_test_actions, JsNativeErrorKind, JsValue, TestAction,
}; };
use indoc::indoc; use indoc::indoc;
@ -60,7 +59,7 @@ fn function_prototype() {
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"new Function.prototype()", "new Function.prototype()",
ErrorKind::Type, JsNativeErrorKind::Type,
"not a constructor", "not a constructor",
), ),
]); ]);
@ -81,7 +80,7 @@ fn function_prototype_call_throw() {
let call = Function.prototype.call; let call = Function.prototype.call;
call(call) call(call)
"#}, "#},
ErrorKind::Type, JsNativeErrorKind::Type,
"undefined is not a function", "undefined is not a function",
)]); )]);
} }
@ -181,12 +180,12 @@ fn function_constructor_early_errors_super() {
run_test_actions([ run_test_actions([
TestAction::assert_native_error( TestAction::assert_native_error(
"Function('super()')()", "Function('super()')()",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"invalid `super` call", "invalid `super` call",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"Function('super.a')()", "Function('super.a')()",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"invalid `super` reference", "invalid `super` reference",
), ),
]); ]);

4
boa_engine/src/builtins/json/tests.rs

@ -1,6 +1,6 @@
use indoc::indoc; use indoc::indoc;
use crate::{builtins::error::ErrorKind, run_test_actions, JsValue, TestAction}; use crate::{run_test_actions, JsNativeErrorKind, JsValue, TestAction};
#[test] #[test]
fn json_sanity() { fn json_sanity() {
@ -307,7 +307,7 @@ fn json_fields_should_be_enumerable() {
fn json_parse_with_no_args_throws_syntax_error() { fn json_parse_with_no_args_throws_syntax_error() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"JSON.parse();", "JSON.parse();",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"expected value at line 1 column 1", "expected value at line 1 column 1",
)]); )]);
} }

4
boa_engine/src/builtins/map/tests.rs

@ -1,4 +1,4 @@
use crate::{builtins::error::ErrorKind, run_test_actions, JsValue, TestAction}; use crate::{run_test_actions, JsNativeErrorKind, JsValue, TestAction};
use indoc::indoc; use indoc::indoc;
#[test] #[test]
@ -242,7 +242,7 @@ fn recursive_display() {
fn not_a_function() { fn not_a_function() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"let map = Map()", "let map = Map()",
ErrorKind::Type, JsNativeErrorKind::Type,
"calling a builtin Map constructor without new is forbidden", "calling a builtin Map constructor without new is forbidden",
)]); )]);
} }

13
boa_engine/src/builtins/number/tests.rs

@ -1,10 +1,7 @@
#![allow(clippy::float_cmp)] #![allow(clippy::float_cmp)]
use crate::{ use crate::{
builtins::{error::ErrorKind, Number}, builtins::Number, run_test_actions, value::AbstractRelation, JsNativeErrorKind, TestAction,
run_test_actions,
value::AbstractRelation,
TestAction,
}; };
#[test] #[test]
@ -81,10 +78,10 @@ fn to_precision() {
"(1/3).toPrecision(60)", "(1/3).toPrecision(60)",
"0.333333333333333314829616256247390992939472198486328125000000", "0.333333333333333314829616256247390992939472198486328125000000",
), ),
TestAction::assert_native_error("(1).toPrecision(101)", ErrorKind::Range, ERROR), TestAction::assert_native_error("(1).toPrecision(101)", JsNativeErrorKind::Range, ERROR),
TestAction::assert_native_error("(1).toPrecision(0)", ErrorKind::Range, ERROR), TestAction::assert_native_error("(1).toPrecision(0)", JsNativeErrorKind::Range, ERROR),
TestAction::assert_native_error("(1).toPrecision(-2000)", ErrorKind::Range, ERROR), TestAction::assert_native_error("(1).toPrecision(-2000)", JsNativeErrorKind::Range, ERROR),
TestAction::assert_native_error("(1).toPrecision('%')", ErrorKind::Range, ERROR), TestAction::assert_native_error("(1).toPrecision('%')", JsNativeErrorKind::Range, ERROR),
]); ]);
} }

40
boa_engine/src/builtins/object/tests.rs

@ -1,4 +1,4 @@
use crate::{builtins::error::ErrorKind, run_test_actions, JsValue, TestAction}; use crate::{run_test_actions, JsNativeErrorKind, JsValue, TestAction};
use indoc::indoc; use indoc::indoc;
#[test] #[test]
@ -15,7 +15,7 @@ fn object_create_with_regular_object() {
fn object_create_with_undefined() { fn object_create_with_undefined() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"Object.create()", "Object.create()",
ErrorKind::Type, JsNativeErrorKind::Type,
"Object prototype may only be an Object or null: undefined", "Object prototype may only be an Object or null: undefined",
)]); )]);
} }
@ -24,7 +24,7 @@ fn object_create_with_undefined() {
fn object_create_with_number() { fn object_create_with_number() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"Object.create(5)", "Object.create(5)",
ErrorKind::Type, JsNativeErrorKind::Type,
"Object prototype may only be an Object or null: 5", "Object prototype may only be an Object or null: 5",
)]); )]);
} }
@ -238,11 +238,19 @@ fn object_get_own_property_names_invalid_args() {
const ERROR: &str = "cannot convert 'null' or 'undefined' to object"; const ERROR: &str = "cannot convert 'null' or 'undefined' to object";
run_test_actions([ run_test_actions([
TestAction::assert_native_error("Object.getOwnPropertyNames()", ErrorKind::Type, ERROR), TestAction::assert_native_error(
TestAction::assert_native_error("Object.getOwnPropertyNames(null)", ErrorKind::Type, ERROR), "Object.getOwnPropertyNames()",
JsNativeErrorKind::Type,
ERROR,
),
TestAction::assert_native_error(
"Object.getOwnPropertyNames(null)",
JsNativeErrorKind::Type,
ERROR,
),
TestAction::assert_native_error( TestAction::assert_native_error(
"Object.getOwnPropertyNames(undefined)", "Object.getOwnPropertyNames(undefined)",
ErrorKind::Type, JsNativeErrorKind::Type,
ERROR, ERROR,
), ),
]); ]);
@ -307,15 +315,19 @@ fn object_get_own_property_symbols_invalid_args() {
const ERROR: &str = "cannot convert 'null' or 'undefined' to object"; const ERROR: &str = "cannot convert 'null' or 'undefined' to object";
run_test_actions([ run_test_actions([
TestAction::assert_native_error("Object.getOwnPropertySymbols()", ErrorKind::Type, ERROR), TestAction::assert_native_error(
"Object.getOwnPropertySymbols()",
JsNativeErrorKind::Type,
ERROR,
),
TestAction::assert_native_error( TestAction::assert_native_error(
"Object.getOwnPropertySymbols(null)", "Object.getOwnPropertySymbols(null)",
ErrorKind::Type, JsNativeErrorKind::Type,
ERROR, ERROR,
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"Object.getOwnPropertySymbols(undefined)", "Object.getOwnPropertySymbols(undefined)",
ErrorKind::Type, JsNativeErrorKind::Type,
ERROR, ERROR,
), ),
]); ]);
@ -382,9 +394,13 @@ fn object_from_entries_invalid_args() {
const ERROR: &str = "cannot convert null or undefined to Object"; const ERROR: &str = "cannot convert null or undefined to Object";
run_test_actions([ run_test_actions([
TestAction::assert_native_error("Object.fromEntries()", ErrorKind::Type, ERROR), TestAction::assert_native_error("Object.fromEntries()", JsNativeErrorKind::Type, ERROR),
TestAction::assert_native_error("Object.fromEntries(null)", ErrorKind::Type, ERROR), TestAction::assert_native_error("Object.fromEntries(null)", JsNativeErrorKind::Type, ERROR),
TestAction::assert_native_error("Object.fromEntries(undefined)", ErrorKind::Type, ERROR), TestAction::assert_native_error(
"Object.fromEntries(undefined)",
JsNativeErrorKind::Type,
ERROR,
),
]); ]);
} }

32
boa_engine/src/builtins/regexp/tests.rs

@ -1,4 +1,4 @@
use crate::{builtins::error::ErrorKind, object::JsObject, run_test_actions, JsValue, TestAction}; use crate::{object::JsObject, run_test_actions, JsNativeErrorKind, JsValue, TestAction};
use indoc::indoc; use indoc::indoc;
#[test] #[test]
@ -120,12 +120,12 @@ fn no_panic_on_parse_fail() {
run_test_actions([ run_test_actions([
TestAction::assert_native_error( TestAction::assert_native_error(
r"var re = /]/u;", r"var re = /]/u;",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid regular expression literal: Unbalanced bracket at line 1, col 10", "Invalid regular expression literal: Unbalanced bracket at line 1, col 10",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
r"var re = /a{/u;", r"var re = /a{/u;",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid regular expression literal: Invalid quantifier at line 1, col 10", "Invalid regular expression literal: Invalid quantifier at line 1, col 10",
), ),
]); ]);
@ -182,13 +182,25 @@ fn search() {
4, 4,
), ),
// this-val-non-obj // this-val-non-obj
TestAction::assert_native_error("search.value.call()", ErrorKind::Type, ERROR), TestAction::assert_native_error("search.value.call()", JsNativeErrorKind::Type, ERROR),
TestAction::assert_native_error("search.value.call(undefined)", ErrorKind::Type, ERROR), TestAction::assert_native_error(
TestAction::assert_native_error("search.value.call(null)", ErrorKind::Type, ERROR), "search.value.call(undefined)",
TestAction::assert_native_error("search.value.call(true)", ErrorKind::Type, ERROR), JsNativeErrorKind::Type,
TestAction::assert_native_error("search.value.call('string')", ErrorKind::Type, ERROR), ERROR,
TestAction::assert_native_error("search.value.call(Symbol.search)", ErrorKind::Type, ERROR), ),
TestAction::assert_native_error("search.value.call(86)", ErrorKind::Type, ERROR), TestAction::assert_native_error("search.value.call(null)", JsNativeErrorKind::Type, ERROR),
TestAction::assert_native_error("search.value.call(true)", JsNativeErrorKind::Type, ERROR),
TestAction::assert_native_error(
"search.value.call('string')",
JsNativeErrorKind::Type,
ERROR,
),
TestAction::assert_native_error(
"search.value.call(Symbol.search)",
JsNativeErrorKind::Type,
ERROR,
),
TestAction::assert_native_error("search.value.call(86)", JsNativeErrorKind::Type, ERROR),
// u-lastindex-advance // u-lastindex-advance
TestAction::assert_eq(r"/\udf06/u[Symbol.search]('\ud834\udf06')", -1), TestAction::assert_eq(r"/\udf06/u[Symbol.search]('\ud834\udf06')", -1),
TestAction::assert_eq("/a/[Symbol.search](\"a\")", 0), TestAction::assert_eq("/a/[Symbol.search](\"a\")", 0),

4
boa_engine/src/builtins/set/tests.rs

@ -1,4 +1,4 @@
use crate::{builtins::error::ErrorKind, run_test_actions, TestAction}; use crate::{run_test_actions, JsNativeErrorKind, TestAction};
use indoc::indoc; use indoc::indoc;
#[test] #[test]
@ -170,7 +170,7 @@ fn recursive_display() {
fn not_a_function() { fn not_a_function() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"Set()", "Set()",
ErrorKind::Type, JsNativeErrorKind::Type,
"calling a builtin Set constructor without new is forbidden", "calling a builtin Set constructor without new is forbidden",
)]); )]);
} }

28
boa_engine/src/builtins/string/tests.rs

@ -1,6 +1,6 @@
use indoc::indoc; use indoc::indoc;
use crate::{builtins::error::ErrorKind, js_string, run_test_actions, JsValue, TestAction}; use crate::{js_string, run_test_actions, JsNativeErrorKind, JsValue, TestAction};
#[test] #[test]
fn length() { fn length() {
@ -104,7 +104,7 @@ fn repeat() {
fn repeat_throws_when_count_is_negative() { fn repeat_throws_when_count_is_negative() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"'x'.repeat(-1)", "'x'.repeat(-1)",
ErrorKind::Range, JsNativeErrorKind::Range,
"repeat count must be a positive finite number \ "repeat count must be a positive finite number \
that doesn't overflow the maximum string length (2^32 - 1)", that doesn't overflow the maximum string length (2^32 - 1)",
)]); )]);
@ -114,7 +114,7 @@ fn repeat_throws_when_count_is_negative() {
fn repeat_throws_when_count_is_infinity() { fn repeat_throws_when_count_is_infinity() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"'x'.repeat(Infinity)", "'x'.repeat(Infinity)",
ErrorKind::Range, JsNativeErrorKind::Range,
"repeat count must be a positive finite number \ "repeat count must be a positive finite number \
that doesn't overflow the maximum string length (2^32 - 1)", that doesn't overflow the maximum string length (2^32 - 1)",
)]); )]);
@ -124,7 +124,7 @@ fn repeat_throws_when_count_is_infinity() {
fn repeat_throws_when_count_overflows_max_length() { fn repeat_throws_when_count_overflows_max_length() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"'x'.repeat(2 ** 64)", "'x'.repeat(2 ** 64)",
ErrorKind::Range, JsNativeErrorKind::Range,
"repeat count must be a positive finite number \ "repeat count must be a positive finite number \
that doesn't overflow the maximum string length (2^32 - 1)", that doesn't overflow the maximum string length (2^32 - 1)",
)]); )]);
@ -247,7 +247,7 @@ fn starts_with() {
fn starts_with_with_regex_arg() { fn starts_with_with_regex_arg() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"'Saturday night'.startsWith(/Saturday/)", "'Saturday night'.startsWith(/Saturday/)",
ErrorKind::Type, JsNativeErrorKind::Type,
"First argument to String.prototype.startsWith must not be a regular expression", "First argument to String.prototype.startsWith must not be a regular expression",
)]); )]);
} }
@ -273,7 +273,7 @@ fn ends_with() {
fn ends_with_with_regex_arg() { fn ends_with_with_regex_arg() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"'Saturday night'.endsWith(/night/)", "'Saturday night'.endsWith(/night/)",
ErrorKind::Type, JsNativeErrorKind::Type,
"First argument to String.prototype.endsWith must not be a regular expression", "First argument to String.prototype.endsWith must not be a regular expression",
)]); )]);
} }
@ -299,7 +299,7 @@ fn includes() {
fn includes_with_regex_arg() { fn includes_with_regex_arg() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"'Saturday night'.includes(/day/)", "'Saturday night'.includes(/day/)",
ErrorKind::Type, JsNativeErrorKind::Type,
"First argument to String.prototype.includes must not be a regular expression", "First argument to String.prototype.includes must not be a regular expression",
)]); )]);
} }
@ -589,7 +589,7 @@ fn split_with_symbol_split_method() {
sep[Symbol.split] = 10; sep[Symbol.split] = 10;
'hello'.split(sep, 10); 'hello'.split(sep, 10);
"#}, "#},
ErrorKind::Type, JsNativeErrorKind::Type,
"value returned for property of object is not a function", "value returned for property of object is not a function",
), ),
]); ]);
@ -880,32 +880,32 @@ fn from_code_point() {
TestAction::assert_eq("String.fromCodePoint(9731, 9733, 9842, 0x4F60)", "☃★♲你"), TestAction::assert_eq("String.fromCodePoint(9731, 9733, 9842, 0x4F60)", "☃★♲你"),
TestAction::assert_native_error( TestAction::assert_native_error(
"String.fromCodePoint('_')", "String.fromCodePoint('_')",
ErrorKind::Range, JsNativeErrorKind::Range,
"codepoint `NaN` is not an integer", "codepoint `NaN` is not an integer",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"String.fromCodePoint(Infinity)", "String.fromCodePoint(Infinity)",
ErrorKind::Range, JsNativeErrorKind::Range,
"codepoint `inf` is not an integer", "codepoint `inf` is not an integer",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"String.fromCodePoint(-1)", "String.fromCodePoint(-1)",
ErrorKind::Range, JsNativeErrorKind::Range,
"codepoint `-1` outside of Unicode range", "codepoint `-1` outside of Unicode range",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"String.fromCodePoint(3.14)", "String.fromCodePoint(3.14)",
ErrorKind::Range, JsNativeErrorKind::Range,
"codepoint `3.14` is not an integer", "codepoint `3.14` is not an integer",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"String.fromCodePoint(3e-2)", "String.fromCodePoint(3e-2)",
ErrorKind::Range, JsNativeErrorKind::Range,
"codepoint `0.03` is not an integer", "codepoint `0.03` is not an integer",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"String.fromCodePoint(NaN)", "String.fromCodePoint(NaN)",
ErrorKind::Range, JsNativeErrorKind::Range,
"codepoint `NaN` is not an integer", "codepoint `NaN` is not an integer",
), ),
]); ]);

2
boa_engine/src/bytecompiler/statement/loop.rs

@ -56,6 +56,7 @@ impl ByteCompiler<'_, '_> {
let (continue_start, continue_exit) = let (continue_start, continue_exit) =
self.emit_opcode_with_two_operands(Opcode::LoopContinue); self.emit_opcode_with_two_operands(Opcode::LoopContinue);
self.patch_jump_with_target(loop_start, start_address); self.patch_jump_with_target(loop_start, start_address);
self.patch_jump_with_target(continue_start, start_address); self.patch_jump_with_target(continue_start, start_address);
@ -345,6 +346,7 @@ impl ByteCompiler<'_, '_> {
let start_address = self.next_opcode_location(); let start_address = self.next_opcode_location();
let (continue_start, continue_exit) = let (continue_start, continue_exit) =
self.emit_opcode_with_two_operands(Opcode::LoopContinue); self.emit_opcode_with_two_operands(Opcode::LoopContinue);
self.push_loop_control_info(label, start_address); self.push_loop_control_info(label, start_address);
self.patch_jump_with_target(loop_start, start_address); self.patch_jump_with_target(loop_start, start_address);
self.patch_jump_with_target(continue_start, start_address); self.patch_jump_with_target(continue_start, start_address);

31
boa_engine/src/context/mod.rs

@ -40,6 +40,8 @@ use boa_interner::{Interner, Sym};
use boa_parser::{Error as ParseError, Parser}; use boa_parser::{Error as ParseError, Parser};
use boa_profiler::Profiler; use boa_profiler::Profiler;
use crate::vm::RuntimeLimits;
/// ECMAScript context. It is the primary way to interact with the runtime. /// ECMAScript context. It is the primary way to interact with the runtime.
/// ///
/// `Context`s constructed in a thread share the same runtime, therefore it /// `Context`s constructed in a thread share the same runtime, therefore it
@ -548,30 +550,36 @@ impl<'host> Context<'host> {
/// Set the value of trace on the context /// Set the value of trace on the context
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
#[inline]
pub fn set_trace(&mut self, trace: bool) { pub fn set_trace(&mut self, trace: bool) {
self.vm.trace = trace; self.vm.trace = trace;
} }
/// Get optimizer options. /// Get optimizer options.
#[inline]
pub const fn optimizer_options(&self) -> OptimizerOptions { pub const fn optimizer_options(&self) -> OptimizerOptions {
self.optimizer_options self.optimizer_options
} }
/// Enable or disable optimizations /// Enable or disable optimizations
#[inline]
pub fn set_optimizer_options(&mut self, optimizer_options: OptimizerOptions) { pub fn set_optimizer_options(&mut self, optimizer_options: OptimizerOptions) {
self.optimizer_options = optimizer_options; self.optimizer_options = optimizer_options;
} }
/// Changes the strictness mode of the context. /// Changes the strictness mode of the context.
#[inline]
pub fn strict(&mut self, strict: bool) { pub fn strict(&mut self, strict: bool) {
self.strict = strict; self.strict = strict;
} }
/// Enqueues a [`NativeJob`] on the [`JobQueue`]. /// Enqueues a [`NativeJob`] on the [`JobQueue`].
#[inline]
pub fn enqueue_job(&mut self, job: NativeJob) { pub fn enqueue_job(&mut self, job: NativeJob) {
self.job_queue().enqueue_promise_job(job, self); self.job_queue().enqueue_promise_job(job, self);
} }
/// Runs all the jobs in the job queue. /// Runs all the jobs in the job queue.
#[inline]
pub fn run_jobs(&mut self) { pub fn run_jobs(&mut self) {
self.job_queue().run_jobs(self); self.job_queue().run_jobs(self);
self.clear_kept_objects(); self.clear_kept_objects();
@ -585,16 +593,19 @@ impl<'host> Context<'host> {
/// [clear]: https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-clear-kept-objects /// [clear]: https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-clear-kept-objects
/// [add]: https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-addtokeptobjects /// [add]: https://tc39.es/ecma262/multipage/executable-code-and-execution-contexts.html#sec-addtokeptobjects
/// [weak]: https://tc39.es/ecma262/multipage/managing-memory.html#sec-weak-ref-objects /// [weak]: https://tc39.es/ecma262/multipage/managing-memory.html#sec-weak-ref-objects
#[inline]
pub fn clear_kept_objects(&mut self) { pub fn clear_kept_objects(&mut self) {
self.kept_alive.clear(); self.kept_alive.clear();
} }
/// Retrieves the current stack trace of the context. /// Retrieves the current stack trace of the context.
#[inline]
pub fn stack_trace(&mut self) -> impl Iterator<Item = &CallFrame> { pub fn stack_trace(&mut self) -> impl Iterator<Item = &CallFrame> {
self.vm.frames.iter().rev() self.vm.frames.iter().rev()
} }
/// Replaces the currently active realm with `realm`, and returns the old realm. /// Replaces the currently active realm with `realm`, and returns the old realm.
#[inline]
pub fn enter_realm(&mut self, realm: Realm) -> Realm { pub fn enter_realm(&mut self, realm: Realm) -> Realm {
self.vm self.vm
.environments .environments
@ -607,14 +618,34 @@ impl<'host> Context<'host> {
} }
/// Gets the host hooks. /// Gets the host hooks.
#[inline]
pub fn host_hooks(&self) -> MaybeShared<'host, dyn HostHooks> { pub fn host_hooks(&self) -> MaybeShared<'host, dyn HostHooks> {
self.host_hooks.clone() self.host_hooks.clone()
} }
/// Gets the job queue. /// Gets the job queue.
#[inline]
pub fn job_queue(&self) -> MaybeShared<'host, dyn JobQueue> { pub fn job_queue(&self) -> MaybeShared<'host, dyn JobQueue> {
self.job_queue.clone() self.job_queue.clone()
} }
/// Get the [`RuntimeLimits`].
#[inline]
pub const fn runtime_limits(&self) -> RuntimeLimits {
self.vm.runtime_limits
}
/// Set the [`RuntimeLimits`].
#[inline]
pub fn set_runtime_limits(&mut self, runtime_limits: RuntimeLimits) {
self.vm.runtime_limits = runtime_limits;
}
/// Get a mutable reference to the [`RuntimeLimits`].
#[inline]
pub fn runtime_limits_mut(&mut self) -> &mut RuntimeLimits {
&mut self.vm.runtime_limits
}
} }
// ==== Private API ==== // ==== Private API ====

8
boa_engine/src/environments/tests.rs

@ -1,4 +1,4 @@
use crate::{builtins::error::ErrorKind, run_test_actions, TestAction}; use crate::{run_test_actions, JsNativeErrorKind, TestAction};
use indoc::indoc; use indoc::indoc;
#[test] #[test]
@ -10,7 +10,7 @@ fn let_is_block_scoped() {
} }
bar; bar;
"#}, "#},
ErrorKind::Reference, JsNativeErrorKind::Reference,
"bar is not defined", "bar is not defined",
)]); )]);
} }
@ -24,7 +24,7 @@ fn const_is_block_scoped() {
} }
bar; bar;
"#}, "#},
ErrorKind::Reference, JsNativeErrorKind::Reference,
"bar is not defined", "bar is not defined",
)]); )]);
} }
@ -51,7 +51,7 @@ fn functions_use_declaration_scope() {
foo(); foo();
} }
"#}, "#},
ErrorKind::Reference, JsNativeErrorKind::Reference,
"bar is not defined", "bar is not defined",
)]); )]);
} }

101
boa_engine/src/error.rs

@ -44,7 +44,7 @@ use thiserror::Error;
/// let kind = &native_error.as_native().unwrap().kind; /// let kind = &native_error.as_native().unwrap().kind;
/// assert!(matches!(kind, JsNativeErrorKind::Type)); /// assert!(matches!(kind, JsNativeErrorKind::Type));
/// ``` /// ```
#[derive(Debug, Clone, Trace, Finalize)] #[derive(Debug, Clone, Trace, Finalize, PartialEq, Eq)]
pub struct JsError { pub struct JsError {
inner: Repr, inner: Repr,
} }
@ -59,7 +59,7 @@ pub struct JsError {
/// This should never be used outside of this module. If that's not the case, /// This should never be used outside of this module. If that's not the case,
/// you should add methods to either `JsError` or `JsNativeError` to /// you should add methods to either `JsError` or `JsNativeError` to
/// represent that special use case. /// represent that special use case.
#[derive(Debug, Clone, Trace, Finalize)] #[derive(Debug, Clone, Trace, Finalize, PartialEq, Eq)]
enum Repr { enum Repr {
Native(JsNativeError), Native(JsNativeError),
Opaque(JsValue), Opaque(JsValue),
@ -417,7 +417,7 @@ impl std::fmt::Display for JsError {
/// ///
/// assert_eq!(native_error.message(), "cannot decode uri"); /// assert_eq!(native_error.message(), "cannot decode uri");
/// ``` /// ```
#[derive(Clone, Trace, Finalize, Error)] #[derive(Clone, Trace, Finalize, Error, PartialEq, Eq)]
#[error("{kind}: {message}")] #[error("{kind}: {message}")]
pub struct JsNativeError { pub struct JsNativeError {
/// The kind of native error (e.g. `TypeError`, `SyntaxError`, etc.) /// The kind of native error (e.g. `TypeError`, `SyntaxError`, etc.)
@ -468,10 +468,17 @@ impl JsNativeError {
/// )); /// ));
/// ``` /// ```
#[must_use] #[must_use]
#[inline]
pub fn aggregate(errors: Vec<JsError>) -> Self { pub fn aggregate(errors: Vec<JsError>) -> Self {
Self::new(JsNativeErrorKind::Aggregate(errors), Box::default(), None) Self::new(JsNativeErrorKind::Aggregate(errors), Box::default(), None)
} }
/// Check if it's a [`JsNativeErrorKind::Aggregate`].
#[inline]
pub const fn is_aggregate(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::Aggregate(_))
}
/// Creates a new `JsNativeError` of kind `Error`, with empty `message` and undefined `cause`. /// Creates a new `JsNativeError` of kind `Error`, with empty `message` and undefined `cause`.
/// ///
/// # Examples /// # Examples
@ -483,10 +490,17 @@ impl JsNativeError {
/// assert!(matches!(error.kind, JsNativeErrorKind::Error)); /// assert!(matches!(error.kind, JsNativeErrorKind::Error));
/// ``` /// ```
#[must_use] #[must_use]
#[inline]
pub fn error() -> Self { pub fn error() -> Self {
Self::new(JsNativeErrorKind::Error, Box::default(), None) Self::new(JsNativeErrorKind::Error, Box::default(), None)
} }
/// Check if it's a [`JsNativeErrorKind::Error`].
#[inline]
pub const fn is_error(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::Error)
}
/// Creates a new `JsNativeError` of kind `EvalError`, with empty `message` and undefined `cause`. /// Creates a new `JsNativeError` of kind `EvalError`, with empty `message` and undefined `cause`.
/// ///
/// # Examples /// # Examples
@ -498,10 +512,17 @@ impl JsNativeError {
/// assert!(matches!(error.kind, JsNativeErrorKind::Eval)); /// assert!(matches!(error.kind, JsNativeErrorKind::Eval));
/// ``` /// ```
#[must_use] #[must_use]
#[inline]
pub fn eval() -> Self { pub fn eval() -> Self {
Self::new(JsNativeErrorKind::Eval, Box::default(), None) Self::new(JsNativeErrorKind::Eval, Box::default(), None)
} }
/// Check if it's a [`JsNativeErrorKind::Eval`].
#[inline]
pub const fn is_eval(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::Eval)
}
/// Creates a new `JsNativeError` of kind `RangeError`, with empty `message` and undefined `cause`. /// Creates a new `JsNativeError` of kind `RangeError`, with empty `message` and undefined `cause`.
/// ///
/// # Examples /// # Examples
@ -513,10 +534,17 @@ impl JsNativeError {
/// assert!(matches!(error.kind, JsNativeErrorKind::Range)); /// assert!(matches!(error.kind, JsNativeErrorKind::Range));
/// ``` /// ```
#[must_use] #[must_use]
#[inline]
pub fn range() -> Self { pub fn range() -> Self {
Self::new(JsNativeErrorKind::Range, Box::default(), None) Self::new(JsNativeErrorKind::Range, Box::default(), None)
} }
/// Check if it's a [`JsNativeErrorKind::Range`].
#[inline]
pub const fn is_range(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::Range)
}
/// Creates a new `JsNativeError` of kind `ReferenceError`, with empty `message` and undefined `cause`. /// Creates a new `JsNativeError` of kind `ReferenceError`, with empty `message` and undefined `cause`.
/// ///
/// # Examples /// # Examples
@ -528,10 +556,17 @@ impl JsNativeError {
/// assert!(matches!(error.kind, JsNativeErrorKind::Reference)); /// assert!(matches!(error.kind, JsNativeErrorKind::Reference));
/// ``` /// ```
#[must_use] #[must_use]
#[inline]
pub fn reference() -> Self { pub fn reference() -> Self {
Self::new(JsNativeErrorKind::Reference, Box::default(), None) Self::new(JsNativeErrorKind::Reference, Box::default(), None)
} }
/// Check if it's a [`JsNativeErrorKind::Reference`].
#[inline]
pub const fn is_reference(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::Reference)
}
/// Creates a new `JsNativeError` of kind `SyntaxError`, with empty `message` and undefined `cause`. /// Creates a new `JsNativeError` of kind `SyntaxError`, with empty `message` and undefined `cause`.
/// ///
/// # Examples /// # Examples
@ -543,10 +578,17 @@ impl JsNativeError {
/// assert!(matches!(error.kind, JsNativeErrorKind::Syntax)); /// assert!(matches!(error.kind, JsNativeErrorKind::Syntax));
/// ``` /// ```
#[must_use] #[must_use]
#[inline]
pub fn syntax() -> Self { pub fn syntax() -> Self {
Self::new(JsNativeErrorKind::Syntax, Box::default(), None) Self::new(JsNativeErrorKind::Syntax, Box::default(), None)
} }
/// Check if it's a [`JsNativeErrorKind::Syntax`].
#[inline]
pub const fn is_syntax(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::Syntax)
}
/// Creates a new `JsNativeError` of kind `TypeError`, with empty `message` and undefined `cause`. /// Creates a new `JsNativeError` of kind `TypeError`, with empty `message` and undefined `cause`.
/// ///
/// # Examples /// # Examples
@ -558,10 +600,17 @@ impl JsNativeError {
/// assert!(matches!(error.kind, JsNativeErrorKind::Type)); /// assert!(matches!(error.kind, JsNativeErrorKind::Type));
/// ``` /// ```
#[must_use] #[must_use]
#[inline]
pub fn typ() -> Self { pub fn typ() -> Self {
Self::new(JsNativeErrorKind::Type, Box::default(), None) Self::new(JsNativeErrorKind::Type, Box::default(), None)
} }
/// Check if it's a [`JsNativeErrorKind::Type`].
#[inline]
pub const fn is_type(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::Type)
}
/// Creates a new `JsNativeError` of kind `UriError`, with empty `message` and undefined `cause`. /// Creates a new `JsNativeError` of kind `UriError`, with empty `message` and undefined `cause`.
/// ///
/// # Examples /// # Examples
@ -573,10 +622,17 @@ impl JsNativeError {
/// assert!(matches!(error.kind, JsNativeErrorKind::Uri)); /// assert!(matches!(error.kind, JsNativeErrorKind::Uri));
/// ``` /// ```
#[must_use] #[must_use]
#[inline]
pub fn uri() -> Self { pub fn uri() -> Self {
Self::new(JsNativeErrorKind::Uri, Box::default(), None) Self::new(JsNativeErrorKind::Uri, Box::default(), None)
} }
/// Check if it's a [`JsNativeErrorKind::Uri`].
#[inline]
pub const fn is_uri(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::Uri)
}
/// Creates a new `JsNativeError` that indicates that the context hit its execution limit. This /// Creates a new `JsNativeError` that indicates that the context hit its execution limit. This
/// is only used in a fuzzing context. /// is only used in a fuzzing context.
#[cfg(feature = "fuzz")] #[cfg(feature = "fuzz")]
@ -589,6 +645,26 @@ impl JsNativeError {
) )
} }
/// Check if it's a [`JsNativeErrorKind::NoInstructionsRemain`].
#[inline]
#[cfg(feature = "fuzz")]
pub const fn is_no_instructions_remain(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::NoInstructionsRemain)
}
/// Creates a new `JsNativeError` that indicates that the context exceeded the runtime limits.
#[must_use]
#[inline]
pub fn runtime_limit() -> Self {
Self::new(JsNativeErrorKind::RuntimeLimit, Box::default(), None)
}
/// Check if it's a [`JsNativeErrorKind::RuntimeLimit`].
#[inline]
pub const fn is_runtime_limit(&self) -> bool {
matches!(self.kind, JsNativeErrorKind::RuntimeLimit)
}
/// Sets the message of this error. /// Sets the message of this error.
/// ///
/// # Examples /// # Examples
@ -600,6 +676,7 @@ impl JsNativeError {
/// assert_eq!(error.message(), "number too large"); /// assert_eq!(error.message(), "number too large");
/// ``` /// ```
#[must_use] #[must_use]
#[inline]
pub fn with_message<S>(mut self, message: S) -> Self pub fn with_message<S>(mut self, message: S) -> Self
where where
S: Into<Box<str>>, S: Into<Box<str>>,
@ -620,6 +697,7 @@ impl JsNativeError {
/// assert!(error.cause().unwrap().as_native().is_some()); /// assert!(error.cause().unwrap().as_native().is_some());
/// ``` /// ```
#[must_use] #[must_use]
#[inline]
pub fn with_cause<V>(mut self, cause: V) -> Self pub fn with_cause<V>(mut self, cause: V) -> Self
where where
V: Into<JsError>, V: Into<JsError>,
@ -644,6 +722,7 @@ impl JsNativeError {
/// assert_eq!(error.message(), "number too large"); /// assert_eq!(error.message(), "number too large");
/// ``` /// ```
#[must_use] #[must_use]
#[inline]
pub const fn message(&self) -> &str { pub const fn message(&self) -> &str {
&self.message &self.message
} }
@ -665,6 +744,7 @@ impl JsNativeError {
/// assert!(error.cause().unwrap().as_native().is_some()); /// assert!(error.cause().unwrap().as_native().is_some());
/// ``` /// ```
#[must_use] #[must_use]
#[inline]
pub fn cause(&self) -> Option<&JsError> { pub fn cause(&self) -> Option<&JsError> {
self.cause.as_deref() self.cause.as_deref()
} }
@ -683,6 +763,11 @@ impl JsNativeError {
/// assert!(error_obj.borrow().is_error()); /// assert!(error_obj.borrow().is_error());
/// assert_eq!(error_obj.get("message", context).unwrap(), "error!".into()) /// assert_eq!(error_obj.get("message", context).unwrap(), "error!".into())
/// ``` /// ```
///
/// # Panics
///
/// If converting a [`JsNativeErrorKind::RuntimeLimit`] to an opaque object.
#[inline]
pub fn to_opaque(&self, context: &mut Context<'_>) -> JsObject { pub fn to_opaque(&self, context: &mut Context<'_>) -> JsObject {
let Self { let Self {
kind, kind,
@ -717,6 +802,9 @@ impl JsNativeError {
"The NoInstructionsRemain native error cannot be converted to an opaque type." "The NoInstructionsRemain native error cannot be converted to an opaque type."
) )
} }
JsNativeErrorKind::RuntimeLimit => {
panic!("The RuntimeLimit native error cannot be converted to an opaque type.")
}
}; };
let o = JsObject::from_proto_and_data_with_shared_shape( let o = JsObject::from_proto_and_data_with_shared_shape(
@ -776,7 +864,7 @@ impl From<boa_parser::Error> for JsNativeError {
/// ///
/// [spec]: https://tc39.es/ecma262/#sec-error-objects /// [spec]: https://tc39.es/ecma262/#sec-error-objects
/// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error
#[derive(Debug, Clone, Trace, Finalize)] #[derive(Debug, Clone, Trace, Finalize, PartialEq, Eq)]
#[non_exhaustive] #[non_exhaustive]
pub enum JsNativeErrorKind { pub enum JsNativeErrorKind {
/// A collection of errors wrapped in a single error. /// A collection of errors wrapped in a single error.
@ -855,10 +943,14 @@ pub enum JsNativeErrorKind {
/// [e_uri]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI /// [e_uri]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURI
/// [d_uri]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI /// [d_uri]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/decodeURI
Uri, Uri,
/// Error thrown when no instructions remain. Only used in a fuzzing context; not a valid JS /// Error thrown when no instructions remain. Only used in a fuzzing context; not a valid JS
/// error variant. /// error variant.
#[cfg(feature = "fuzz")] #[cfg(feature = "fuzz")]
NoInstructionsRemain, NoInstructionsRemain,
/// Error thrown when a runtime limit is exceeded. It's not a valid JS error variant.
RuntimeLimit,
} }
impl PartialEq<ErrorKind> for JsNativeErrorKind { impl PartialEq<ErrorKind> for JsNativeErrorKind {
@ -888,6 +980,7 @@ impl std::fmt::Display for JsNativeErrorKind {
Self::Syntax => "SyntaxError", Self::Syntax => "SyntaxError",
Self::Type => "TypeError", Self::Type => "TypeError",
Self::Uri => "UriError", Self::Uri => "UriError",
Self::RuntimeLimit => "RuntimeLimit",
#[cfg(feature = "fuzz")] #[cfg(feature = "fuzz")]
Self::NoInstructionsRemain => "NoInstructionsRemain", Self::NoInstructionsRemain => "NoInstructionsRemain",
} }

4
boa_engine/src/lib.rs

@ -257,7 +257,7 @@ enum Inner {
}, },
AssertNativeError { AssertNativeError {
source: Cow<'static, str>, source: Cow<'static, str>,
kind: builtins::error::ErrorKind, kind: JsNativeErrorKind,
message: &'static str, message: &'static str,
}, },
AssertContext { AssertContext {
@ -328,7 +328,7 @@ impl TestAction {
/// Asserts that evaluating `source` throws a native error of `kind` and `message`. /// Asserts that evaluating `source` throws a native error of `kind` and `message`.
fn assert_native_error( fn assert_native_error(
source: impl Into<Cow<'static, str>>, source: impl Into<Cow<'static, str>>,
kind: builtins::error::ErrorKind, kind: JsNativeErrorKind,
message: &'static str, message: &'static str,
) -> Self { ) -> Self {
Self(Inner::AssertNativeError { Self(Inner::AssertNativeError {

4
boa_engine/src/object/tests.rs

@ -1,4 +1,4 @@
use crate::{builtins::error::ErrorKind, run_test_actions, TestAction}; use crate::{run_test_actions, JsNativeErrorKind, TestAction};
use indoc::indoc; use indoc::indoc;
#[test] #[test]
@ -9,7 +9,7 @@ fn ordinary_has_instance_nonobject_prototype() {
C.prototype = 1 C.prototype = 1
String instanceof C String instanceof C
"#}, "#},
ErrorKind::Type, JsNativeErrorKind::Type,
"function has non-object prototype in instanceof check", "function has non-object prototype in instanceof check",
)]); )]);
} }

14
boa_engine/src/realm.rs

@ -24,6 +24,14 @@ pub struct Realm {
inner: Gc<Inner>, inner: Gc<Inner>,
} }
impl Eq for Realm {}
impl PartialEq for Realm {
fn eq(&self, other: &Self) -> bool {
Gc::ptr_eq(&self.inner, &other.inner)
}
}
impl fmt::Debug for Realm { impl fmt::Debug for Realm {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Realm") f.debug_struct("Realm")
@ -35,12 +43,6 @@ impl fmt::Debug for Realm {
} }
} }
impl PartialEq for Realm {
fn eq(&self, other: &Self) -> bool {
std::ptr::eq(&*self.inner, &*other.inner)
}
}
#[derive(Trace, Finalize)] #[derive(Trace, Finalize)]
struct Inner { struct Inner {
intrinsics: Intrinsics, intrinsics: Intrinsics,

10
boa_engine/src/tests/control_flow/loops.rs

@ -1,4 +1,4 @@
use crate::{builtins::error::ErrorKind, run_test_actions, TestAction}; use crate::{run_test_actions, JsNativeErrorKind, TestAction};
use indoc::indoc; use indoc::indoc;
#[test] #[test]
@ -125,7 +125,7 @@ fn for_loop_iteration_variable_does_not_leak() {
for (let i = 0;false;) {} for (let i = 0;false;) {}
i i
"#}, "#},
ErrorKind::Reference, JsNativeErrorKind::Reference,
"i is not defined", "i is not defined",
)]); )]);
} }
@ -138,7 +138,7 @@ fn test_invalid_break_target() {
break nonexistent; break nonexistent;
} }
"#}, "#},
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"undefined break target: nonexistent at line 1, col 1", "undefined break target: nonexistent at line 1, col 1",
)]); )]);
} }
@ -625,7 +625,7 @@ fn for_of_loop_let() {
} }
"#}), "#}),
TestAction::assert_eq("result", 3), TestAction::assert_eq("result", 3),
TestAction::assert_native_error("i", ErrorKind::Reference, "i is not defined"), TestAction::assert_native_error("i", JsNativeErrorKind::Reference, "i is not defined"),
]); ]);
} }
@ -639,7 +639,7 @@ fn for_of_loop_const() {
} }
"#}), "#}),
TestAction::assert_eq("result", 3), TestAction::assert_eq("result", 3),
TestAction::assert_native_error("i", ErrorKind::Reference, "i is not defined"), TestAction::assert_native_error("i", JsNativeErrorKind::Reference, "i is not defined"),
]); ]);
} }

8
boa_engine/src/tests/control_flow/mod.rs

@ -1,13 +1,13 @@
use indoc::indoc; use indoc::indoc;
mod loops; mod loops;
use crate::{builtins::error::ErrorKind, run_test_actions, TestAction}; use crate::{run_test_actions, JsNativeErrorKind, TestAction};
#[test] #[test]
fn test_invalid_break() { fn test_invalid_break() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"break;", "break;",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"illegal break statement at line 1, col 1", "illegal break statement at line 1, col 1",
)]); )]);
} }
@ -20,7 +20,7 @@ fn test_invalid_continue_target() {
continue nonexistent; continue nonexistent;
} }
"#}, "#},
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"undefined continue target: nonexistent at line 1, col 1", "undefined continue target: nonexistent at line 1, col 1",
)]); )]);
} }
@ -29,7 +29,7 @@ fn test_invalid_continue_target() {
fn test_invalid_continue() { fn test_invalid_continue() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"continue;", "continue;",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"illegal continue statement at line 1, col 1", "illegal continue statement at line 1, col 1",
)]); )]);
} }

16
boa_engine/src/tests/function.rs

@ -1,4 +1,4 @@
use crate::{builtins::error::ErrorKind, run_test_actions, JsValue, TestAction}; use crate::{run_test_actions, JsNativeErrorKind, JsValue, TestAction};
use indoc::indoc; use indoc::indoc;
#[test] #[test]
@ -87,7 +87,7 @@ fn should_set_this_value() {
fn should_type_error_when_new_is_not_constructor() { fn should_type_error_when_new_is_not_constructor() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"new ''()", "new ''()",
ErrorKind::Type, JsNativeErrorKind::Type,
"not a constructor", "not a constructor",
)]); )]);
} }
@ -125,9 +125,13 @@ fn not_a_function() {
let a = {}; let a = {};
let b = true; let b = true;
"#}), "#}),
TestAction::assert_native_error("a()", ErrorKind::Type, "not a callable function"), TestAction::assert_native_error("a()", JsNativeErrorKind::Type, "not a callable function"),
TestAction::assert_native_error("a.a()", ErrorKind::Type, "not a callable function"), TestAction::assert_native_error(
TestAction::assert_native_error("b()", ErrorKind::Type, "not a callable function"), "a.a()",
JsNativeErrorKind::Type,
"not a callable function",
),
TestAction::assert_native_error("b()", JsNativeErrorKind::Type, "not a callable function"),
]); ]);
} }
@ -140,7 +144,7 @@ fn strict_mode_dup_func_parameters() {
'use strict'; 'use strict';
function f(a, b, b) {} function f(a, b, b) {}
"#}, "#},
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Duplicate parameter name not allowed in this context at line 2, col 12", "Duplicate parameter name not allowed in this context at line 2, col 12",
)]); )]);
} }

16
boa_engine/src/tests/mod.rs

@ -7,7 +7,7 @@ mod operators;
mod promise; mod promise;
mod spread; mod spread;
use crate::{builtins::error::ErrorKind, run_test_actions, JsValue, TestAction}; use crate::{run_test_actions, JsNativeErrorKind, JsValue, TestAction};
#[test] #[test]
fn length_correct_value_on_string_literal() { fn length_correct_value_on_string_literal() {
@ -40,7 +40,7 @@ fn empty_var_decl_undefined() {
fn identifier_on_global_object_undefined() { fn identifier_on_global_object_undefined() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"bar;", "bar;",
ErrorKind::Reference, JsNativeErrorKind::Reference,
"bar is not defined", "bar is not defined",
)]); )]);
} }
@ -372,7 +372,7 @@ fn undefined_constant() {
fn identifier_op() { fn identifier_op() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"break = 1", "break = 1",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
r#"expected token 'identifier', got '=' in identifier parsing at line 1, col 7"#, r#"expected token 'identifier', got '=' in identifier parsing at line 1, col 7"#,
)]); )]);
} }
@ -386,7 +386,7 @@ fn strict_mode_octal() {
'use strict'; 'use strict';
var n = 023; var n = 023;
"#}, "#},
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"implicit octal literals are not allowed in strict mode at line 2, col 9", "implicit octal literals are not allowed in strict mode at line 2, col 9",
)]); )]);
} }
@ -404,7 +404,7 @@ fn strict_mode_with() {
} }
} }
"#}, "#},
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"with statement not allowed in strict mode at line 3, col 5", "with statement not allowed in strict mode at line 3, col 5",
)]); )]);
} }
@ -429,7 +429,11 @@ fn strict_mode_reserved_name() {
]; ];
run_test_actions(cases.into_iter().map(|(case, msg)| { run_test_actions(cases.into_iter().map(|(case, msg)| {
TestAction::assert_native_error(format!("'use strict'; {case}"), ErrorKind::Syntax, msg) TestAction::assert_native_error(
format!("'use strict'; {case}"),
JsNativeErrorKind::Syntax,
msg,
)
})); }));
} }

60
boa_engine/src/tests/operators.rs

@ -1,4 +1,4 @@
use crate::{builtins::error::ErrorKind, run_test_actions, JsValue, TestAction}; use crate::{run_test_actions, JsNativeErrorKind, JsValue, TestAction};
use indoc::indoc; use indoc::indoc;
#[test] #[test]
@ -143,22 +143,22 @@ fn invalid_unary_access() {
run_test_actions([ run_test_actions([
TestAction::assert_native_error( TestAction::assert_native_error(
"++[]", "++[]",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 1", "Invalid left-hand side in assignment at line 1, col 1",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"[]++", "[]++",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 3", "Invalid left-hand side in assignment at line 1, col 3",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"--[]", "--[]",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 1", "Invalid left-hand side in assignment at line 1, col 1",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"[]--", "[]--",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 3", "Invalid left-hand side in assignment at line 1, col 3",
), ),
]); ]);
@ -170,22 +170,22 @@ fn unary_operations_on_this() {
run_test_actions([ run_test_actions([
TestAction::assert_native_error( TestAction::assert_native_error(
"++this", "++this",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 1", "Invalid left-hand side in assignment at line 1, col 1",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"--this", "--this",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 1", "Invalid left-hand side in assignment at line 1, col 1",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"this++", "this++",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 5", "Invalid left-hand side in assignment at line 1, col 5",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"this--", "this--",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 5", "Invalid left-hand side in assignment at line 1, col 5",
), ),
]); ]);
@ -305,7 +305,7 @@ fn assignment_to_non_assignable() {
.map(|src| { .map(|src| {
TestAction::assert_native_error( TestAction::assert_native_error(
src, src,
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 3", "Invalid left-hand side in assignment at line 1, col 3",
) )
}), }),
@ -330,7 +330,7 @@ fn assignment_to_non_assignable_ctd() {
.map(|src| { .map(|src| {
TestAction::assert_native_error( TestAction::assert_native_error(
src, src,
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 13", "Invalid left-hand side in assignment at line 1, col 13",
) )
}), }),
@ -344,7 +344,7 @@ fn multicharacter_assignment_to_non_assignable() {
run_test_actions(["3 **= 5", "3 <<= 5", "3 >>= 5"].into_iter().map(|src| { run_test_actions(["3 **= 5", "3 <<= 5", "3 >>= 5"].into_iter().map(|src| {
TestAction::assert_native_error( TestAction::assert_native_error(
src, src,
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 3", "Invalid left-hand side in assignment at line 1, col 3",
) )
})); }));
@ -358,7 +358,7 @@ fn multicharacter_assignment_to_non_assignable_ctd() {
.map(|src| { .map(|src| {
TestAction::assert_native_error( TestAction::assert_native_error(
src, src,
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 13", "Invalid left-hand side in assignment at line 1, col 13",
) )
}), }),
@ -373,7 +373,7 @@ fn multicharacter_bitwise_assignment_to_non_assignable() {
.map(|src| { .map(|src| {
TestAction::assert_native_error( TestAction::assert_native_error(
src, src,
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 3", "Invalid left-hand side in assignment at line 1, col 3",
) )
}), }),
@ -393,7 +393,7 @@ fn multicharacter_bitwise_assignment_to_non_assignable_ctd() {
.map(|src| { .map(|src| {
TestAction::assert_native_error( TestAction::assert_native_error(
src, src,
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 13", "Invalid left-hand side in assignment at line 1, col 13",
) )
}), }),
@ -405,22 +405,22 @@ fn assign_to_array_decl() {
run_test_actions([ run_test_actions([
TestAction::assert_native_error( TestAction::assert_native_error(
"[1] = [2]", "[1] = [2]",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 5", "Invalid left-hand side in assignment at line 1, col 5",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"[3, 5] = [7, 8]", "[3, 5] = [7, 8]",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 8", "Invalid left-hand side in assignment at line 1, col 8",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"[6, 8] = [2]", "[6, 8] = [2]",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 8", "Invalid left-hand side in assignment at line 1, col 8",
), ),
TestAction::assert_native_error( TestAction::assert_native_error(
"[6] = [2, 9]", "[6] = [2, 9]",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"Invalid left-hand side in assignment at line 1, col 5", "Invalid left-hand side in assignment at line 1, col 5",
), ),
]); ]);
@ -430,7 +430,7 @@ fn assign_to_array_decl() {
fn assign_to_object_decl() { fn assign_to_object_decl() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"{a: 3} = {a: 5};", "{a: 3} = {a: 5};",
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"unexpected token '=', primary expression at line 1, col 8", "unexpected token '=', primary expression at line 1, col 8",
)]); )]);
} }
@ -439,7 +439,7 @@ fn assign_to_object_decl() {
fn assignmentoperator_lhs_not_defined() { fn assignmentoperator_lhs_not_defined() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"a += 5", "a += 5",
ErrorKind::Reference, JsNativeErrorKind::Reference,
"a is not defined", "a is not defined",
)]); )]);
} }
@ -448,7 +448,7 @@ fn assignmentoperator_lhs_not_defined() {
fn assignmentoperator_rhs_throws_error() { fn assignmentoperator_rhs_throws_error() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"let a; a += b", "let a; a += b",
ErrorKind::Reference, JsNativeErrorKind::Reference,
"b is not defined", "b is not defined",
)]); )]);
} }
@ -457,7 +457,7 @@ fn assignmentoperator_rhs_throws_error() {
fn instanceofoperator_rhs_not_object() { fn instanceofoperator_rhs_not_object() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"let s = new String(); s instanceof 1", "let s = new String(); s instanceof 1",
ErrorKind::Type, JsNativeErrorKind::Type,
"right-hand side of 'instanceof' should be an object, got `number`", "right-hand side of 'instanceof' should be an object, got `number`",
)]); )]);
} }
@ -466,7 +466,7 @@ fn instanceofoperator_rhs_not_object() {
fn instanceofoperator_rhs_not_callable() { fn instanceofoperator_rhs_not_callable() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"let s = new String(); s instanceof {}", "let s = new String(); s instanceof {}",
ErrorKind::Type, JsNativeErrorKind::Type,
"right-hand side of 'instanceof' is not callable", "right-hand side of 'instanceof' is not callable",
)]); )]);
} }
@ -504,7 +504,7 @@ fn delete_variable_in_strict() {
let x = 10; let x = 10;
delete x; delete x;
"#}, "#},
ErrorKind::Syntax, JsNativeErrorKind::Syntax,
"cannot delete variables in strict mode at line 3, col 1", "cannot delete variables in strict mode at line 3, col 1",
)]); )]);
} }
@ -513,7 +513,7 @@ fn delete_variable_in_strict() {
fn delete_non_configurable() { fn delete_non_configurable() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"'use strict'; delete Boolean.prototype", "'use strict'; delete Boolean.prototype",
ErrorKind::Type, JsNativeErrorKind::Type,
"Cannot delete property", "Cannot delete property",
)]); )]);
} }
@ -528,7 +528,7 @@ fn delete_non_configurable_in_function() {
} }
t() t()
"#}, "#},
ErrorKind::Type, JsNativeErrorKind::Type,
"Cannot delete property", "Cannot delete property",
)]); )]);
} }
@ -557,7 +557,7 @@ fn delete_in_function_global_strict() {
} }
a(); a();
"#}, "#},
ErrorKind::Type, JsNativeErrorKind::Type,
"Cannot delete property", "Cannot delete property",
)]); )]);
} }
@ -591,7 +591,7 @@ fn delete_in_strict_function_returned() {
} }
a()(); a()();
"#}, "#},
ErrorKind::Type, JsNativeErrorKind::Type,
"Cannot delete property", "Cannot delete property",
)]); )]);
} }
@ -633,7 +633,7 @@ mod in_operator {
fn should_type_error_when_rhs_not_object() { fn should_type_error_when_rhs_not_object() {
run_test_actions([TestAction::assert_native_error( run_test_actions([TestAction::assert_native_error(
"'fail' in undefined", "'fail' in undefined",
ErrorKind::Type, JsNativeErrorKind::Type,
"right-hand side of 'in' should be an object, got `undefined`", "right-hand side of 'in' should be an object, got `undefined`",
)]); )]);
} }

4
boa_engine/src/tests/spread.rs

@ -1,4 +1,4 @@
use crate::{builtins::error::ErrorKind, run_test_actions, JsValue, TestAction}; use crate::{run_test_actions, JsNativeErrorKind, JsValue, TestAction};
use indoc::indoc; use indoc::indoc;
#[test] #[test]
@ -97,7 +97,7 @@ fn spread_getters_in_object() {
var a = { x: 42 }; var a = { x: 42 };
var aWithXGetter = { ...a, ... { get x() { throw new Error('not thrown yet') } } }; var aWithXGetter = { ...a, ... { get x() { throw new Error('not thrown yet') } } };
"#}, "#},
ErrorKind::Error, JsNativeErrorKind::Error,
"not thrown yet", "not thrown yet",
)]); )]);
} }

4
boa_engine/src/value/tests.rs

@ -700,7 +700,7 @@ fn to_bigint() {
/// Relevant mitigation for these are in `JsObject::ordinary_to_primitive` and /// Relevant mitigation for these are in `JsObject::ordinary_to_primitive` and
/// `JsObject::to_json` /// `JsObject::to_json`
mod cyclic_conversions { mod cyclic_conversions {
use crate::builtins::error::ErrorKind; use crate::JsNativeErrorKind;
use super::*; use super::*;
@ -712,7 +712,7 @@ mod cyclic_conversions {
a[0] = a; a[0] = a;
JSON.stringify(a) JSON.stringify(a)
"#}, "#},
ErrorKind::Type, JsNativeErrorKind::Type,
"cyclic object value", "cyclic object value",
)]); )]);
} }

21
boa_engine/src/vm/call_frame/env_stack.rs

@ -3,7 +3,10 @@
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum EnvEntryKind { pub(crate) enum EnvEntryKind {
Global, Global,
Loop, Loop {
/// This is used to keep track of how many iterations a loop has done.
iteration_count: u64,
},
Try, Try,
Catch, Catch,
Finally, Finally,
@ -49,8 +52,9 @@ impl EnvStackEntry {
} }
/// Returns calling `EnvStackEntry` with `kind` field of `Loop`. /// Returns calling `EnvStackEntry` with `kind` field of `Loop`.
pub(crate) const fn with_loop_flag(mut self) -> Self { /// And the loop iteration set to zero.
self.kind = EnvEntryKind::Loop; pub(crate) const fn with_loop_flag(mut self, iteration_count: u64) -> Self {
self.kind = EnvEntryKind::Loop { iteration_count };
self self
} }
@ -95,8 +99,15 @@ impl EnvStackEntry {
} }
/// Returns true if an `EnvStackEntry` is a loop /// Returns true if an `EnvStackEntry` is a loop
pub(crate) fn is_loop_env(&self) -> bool { pub(crate) const fn is_loop_env(&self) -> bool {
self.kind == EnvEntryKind::Loop matches!(self.kind, EnvEntryKind::Loop { .. })
}
pub(crate) const fn as_loop_iteration_count(self) -> Option<u64> {
if let EnvEntryKind::Loop { iteration_count } = self.kind {
return Some(iteration_count);
}
None
} }
/// Returns true if an `EnvStackEntry` is a try block /// Returns true if an `EnvStackEntry` is a try block

1
boa_engine/src/vm/flowgraph/mod.rs

@ -108,7 +108,6 @@ impl CodeBlock {
EdgeStyle::Line, EdgeStyle::Line,
); );
} }
Opcode::JumpIfFalse Opcode::JumpIfFalse
| Opcode::JumpIfTrue | Opcode::JumpIfTrue
| Opcode::JumpIfNotUndefined | Opcode::JumpIfNotUndefined

24
boa_engine/src/vm/mod.rs

@ -4,14 +4,15 @@
//! This module will provide an instruction set for the AST to use, various traits, //! This module will provide an instruction set for the AST to use, various traits,
//! plus an interpreter to execute those instructions //! plus an interpreter to execute those instructions
#[cfg(feature = "fuzz")]
use crate::JsNativeError;
use crate::{ use crate::{
builtins::async_generator::{AsyncGenerator, AsyncGeneratorState}, builtins::async_generator::{AsyncGenerator, AsyncGeneratorState},
environments::{DeclarativeEnvironment, DeclarativeEnvironmentStack}, environments::{DeclarativeEnvironment, DeclarativeEnvironmentStack},
vm::code_block::Readable, vm::code_block::Readable,
Context, JsError, JsObject, JsResult, JsValue, Context, JsError, JsObject, JsResult, JsValue,
}; };
#[cfg(feature = "fuzz")]
use crate::{JsNativeError, JsNativeErrorKind};
use boa_gc::Gc; use boa_gc::Gc;
use boa_profiler::Profiler; use boa_profiler::Profiler;
use std::{convert::TryInto, mem::size_of}; use std::{convert::TryInto, mem::size_of};
@ -26,9 +27,12 @@ mod code_block;
mod completion_record; mod completion_record;
mod opcode; mod opcode;
mod runtime_limits;
#[cfg(feature = "flowgraph")] #[cfg(feature = "flowgraph")]
pub mod flowgraph; pub mod flowgraph;
pub use runtime_limits::RuntimeLimits;
pub use {call_frame::CallFrame, code_block::CodeBlock, opcode::Opcode}; pub use {call_frame::CallFrame, code_block::CodeBlock, opcode::Opcode};
pub(crate) use { pub(crate) use {
@ -52,7 +56,7 @@ pub struct Vm {
pub(crate) environments: DeclarativeEnvironmentStack, pub(crate) environments: DeclarativeEnvironmentStack,
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
pub(crate) trace: bool, pub(crate) trace: bool,
pub(crate) stack_size_limit: usize, pub(crate) runtime_limits: RuntimeLimits,
pub(crate) active_function: Option<JsObject>, pub(crate) active_function: Option<JsObject>,
} }
@ -66,7 +70,7 @@ impl Vm {
err: None, err: None,
#[cfg(feature = "trace")] #[cfg(feature = "trace")]
trace: false, trace: false,
stack_size_limit: 1024, runtime_limits: RuntimeLimits::default(),
active_function: None, active_function: None,
} }
} }
@ -268,14 +272,22 @@ impl Context<'_> {
if let Some(native_error) = err.as_native() { if let Some(native_error) = err.as_native() {
// If we hit the execution step limit, bubble up the error to the // If we hit the execution step limit, bubble up the error to the
// (Rust) caller instead of trying to handle as an exception. // (Rust) caller instead of trying to handle as an exception.
if matches!(native_error.kind, JsNativeErrorKind::NoInstructionsRemain) if native_error.is_no_instructions_remain() {
{
self.vm.err = Some(err); self.vm.err = Some(err);
break CompletionType::Throw; break CompletionType::Throw;
} }
} }
} }
if let Some(native_error) = err.as_native() {
// If we hit the execution step limit, bubble up the error to the
// (Rust) caller instead of trying to handle as an exception.
if native_error.is_runtime_limit() {
self.vm.err = Some(err);
break CompletionType::Throw;
}
}
self.vm.err = Some(err); self.vm.err = Some(err);
// If this frame has not evaluated the throw as an AbruptCompletion, then evaluate it // If this frame has not evaluated the throw as an AbruptCompletion, then evaluate it

16
boa_engine/src/vm/opcode/call/mod.rs

@ -17,8 +17,8 @@ impl Operation for CallEval {
const INSTRUCTION: &'static str = "INST - CallEval"; const INSTRUCTION: &'static str = "INST - CallEval";
fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> { fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> {
if context.vm.stack_size_limit <= context.vm.stack.len() { if context.vm.runtime_limits.stack_size_limit() <= context.vm.stack.len() {
return Err(JsNativeError::range() return Err(JsNativeError::runtime_limit()
.with_message("Maximum call stack size exceeded") .with_message("Maximum call stack size exceeded")
.into()); .into());
} }
@ -77,8 +77,8 @@ impl Operation for CallEvalSpread {
const INSTRUCTION: &'static str = "INST - CallEvalSpread"; const INSTRUCTION: &'static str = "INST - CallEvalSpread";
fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> { fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> {
if context.vm.stack_size_limit <= context.vm.stack.len() { if context.vm.runtime_limits.stack_size_limit() <= context.vm.stack.len() {
return Err(JsNativeError::range() return Err(JsNativeError::runtime_limit()
.with_message("Maximum call stack size exceeded") .with_message("Maximum call stack size exceeded")
.into()); .into());
} }
@ -143,8 +143,8 @@ impl Operation for Call {
const INSTRUCTION: &'static str = "INST - Call"; const INSTRUCTION: &'static str = "INST - Call";
fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> { fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> {
if context.vm.stack_size_limit <= context.vm.stack.len() { if context.vm.runtime_limits.stack_size_limit() <= context.vm.stack.len() {
return Err(JsNativeError::range() return Err(JsNativeError::runtime_limit()
.with_message("Maximum call stack size exceeded") .with_message("Maximum call stack size exceeded")
.into()); .into());
} }
@ -182,8 +182,8 @@ impl Operation for CallSpread {
const INSTRUCTION: &'static str = "INST - CallSpread"; const INSTRUCTION: &'static str = "INST - CallSpread";
fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> { fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> {
if context.vm.stack_size_limit <= context.vm.stack.len() { if context.vm.runtime_limits.stack_size_limit() <= context.vm.stack.len() {
return Err(JsNativeError::range() return Err(JsNativeError::runtime_limit()
.with_message("Maximum call stack size exceeded") .with_message("Maximum call stack size exceeded")
.into()); .into());
} }

65
boa_engine/src/vm/opcode/iteration/loop_ops.rs

@ -1,3 +1,4 @@
use crate::JsNativeError;
use crate::{ use crate::{
vm::{call_frame::EnvStackEntry, opcode::Operation, CompletionType}, vm::{call_frame::EnvStackEntry, opcode::Operation, CompletionType},
Context, JsResult, Context, JsResult,
@ -18,15 +19,29 @@ impl Operation for LoopStart {
let start = context.vm.read::<u32>(); let start = context.vm.read::<u32>();
let exit = context.vm.read::<u32>(); let exit = context.vm.read::<u32>();
context // Create and push loop evironment entry.
.vm let entry = EnvStackEntry::new(start, exit).with_loop_flag(1);
.frame_mut() context.vm.frame_mut().env_stack.push(entry);
.env_stack
.push(EnvStackEntry::new(start, exit).with_loop_flag());
Ok(CompletionType::Normal) Ok(CompletionType::Normal)
} }
} }
/// This is a helper function used to clean the loop environment created by the
/// [`LoopStart`] and [`LoopContinue`] opcodes.
fn cleanup_loop_environment(context: &mut Context<'_>) {
let mut envs_to_pop = 0_usize;
while let Some(env_entry) = context.vm.frame_mut().env_stack.pop() {
envs_to_pop += env_entry.env_num();
if env_entry.is_loop_env() {
break;
}
}
let env_truncation_len = context.vm.environments.len().saturating_sub(envs_to_pop);
context.vm.environments.truncate(env_truncation_len);
}
/// `LoopContinue` implements the Opcode Operation for `Opcode::LoopContinue`. /// `LoopContinue` implements the Opcode Operation for `Opcode::LoopContinue`.
/// ///
/// Operation: /// Operation:
@ -42,6 +57,8 @@ impl Operation for LoopContinue {
let start = context.vm.read::<u32>(); let start = context.vm.read::<u32>();
let exit = context.vm.read::<u32>(); let exit = context.vm.read::<u32>();
let mut iteration_count = 0;
// 1. Clean up the previous environment. // 1. Clean up the previous environment.
if let Some(entry) = context if let Some(entry) = context
.vm .vm
@ -57,15 +74,28 @@ impl Operation for LoopContinue {
.saturating_sub(entry.env_num()); .saturating_sub(entry.env_num());
context.vm.environments.truncate(env_truncation_len); context.vm.environments.truncate(env_truncation_len);
context.vm.frame_mut().env_stack.pop(); // Pop loop environment and get it's iteration count.
let previous_entry = context.vm.frame_mut().env_stack.pop();
if let Some(previous_iteration_count) =
previous_entry.and_then(EnvStackEntry::as_loop_iteration_count)
{
iteration_count = previous_iteration_count.wrapping_add(1);
let max = context.vm.runtime_limits.loop_iteration_limit();
if previous_iteration_count > max {
cleanup_loop_environment(context);
return Err(JsNativeError::runtime_limit()
.with_message(format!("max loop iteration limit {max} exceeded"))
.into());
}
}
} }
// 2. Push a new clean EnvStack. // 2. Push a new clean EnvStack.
context let entry = EnvStackEntry::new(start, exit).with_loop_flag(iteration_count);
.vm
.frame_mut() context.vm.frame_mut().env_stack.push(entry);
.env_stack
.push(EnvStackEntry::new(start, exit).with_loop_flag());
Ok(CompletionType::Normal) Ok(CompletionType::Normal)
} }
@ -83,18 +113,7 @@ impl Operation for LoopEnd {
const INSTRUCTION: &'static str = "INST - LoopEnd"; const INSTRUCTION: &'static str = "INST - LoopEnd";
fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> { fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> {
let mut envs_to_pop = 0_usize; cleanup_loop_environment(context);
while let Some(env_entry) = context.vm.frame_mut().env_stack.pop() {
envs_to_pop += env_entry.env_num();
if env_entry.is_loop_env() {
break;
}
}
let env_truncation_len = context.vm.environments.len().saturating_sub(envs_to_pop);
context.vm.environments.truncate(env_truncation_len);
Ok(CompletionType::Normal) Ok(CompletionType::Normal)
} }
} }

8
boa_engine/src/vm/opcode/new/mod.rs

@ -16,8 +16,8 @@ impl Operation for New {
const INSTRUCTION: &'static str = "INST - New"; const INSTRUCTION: &'static str = "INST - New";
fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> { fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> {
if context.vm.stack_size_limit <= context.vm.stack.len() { if context.vm.runtime_limits.stack_size_limit() <= context.vm.stack.len() {
return Err(JsNativeError::range() return Err(JsNativeError::runtime_limit()
.with_message("Maximum call stack size exceeded") .with_message("Maximum call stack size exceeded")
.into()); .into());
} }
@ -55,8 +55,8 @@ impl Operation for NewSpread {
const INSTRUCTION: &'static str = "INST - NewSpread"; const INSTRUCTION: &'static str = "INST - NewSpread";
fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> { fn execute(context: &mut Context<'_>) -> JsResult<CompletionType> {
if context.vm.stack_size_limit <= context.vm.stack.len() { if context.vm.runtime_limits.stack_size_limit() <= context.vm.stack.len() {
return Err(JsNativeError::range() return Err(JsNativeError::runtime_limit()
.with_message("Maximum call stack size exceeded") .with_message("Maximum call stack size exceeded")
.into()); .into());
} }

61
boa_engine/src/vm/runtime_limits.rs

@ -0,0 +1,61 @@
/// Represents the limits of different runtime operations.
#[derive(Debug, Clone, Copy)]
pub struct RuntimeLimits {
/// Max stack size before an error is thrown.
stack_size_limit: usize,
/// Max loop iterations before an error is thrown.
loop_iteration_limit: u64,
}
impl Default for RuntimeLimits {
#[inline]
fn default() -> Self {
Self {
loop_iteration_limit: u64::MAX,
stack_size_limit: 1024,
}
}
}
impl RuntimeLimits {
/// Return the loop iteration limit.
///
/// If the limit is exceeded in a loop it will throw and errror.
///
/// The limit value [`u64::MAX`] means that there is no limit.
#[inline]
#[must_use]
pub const fn loop_iteration_limit(&self) -> u64 {
self.loop_iteration_limit
}
/// Set the loop iteration limit.
///
/// If the limit is exceeded in a loop it will throw and errror.
///
/// Setting the limit to [`u64::MAX`] means that there is no limit.
#[inline]
pub fn set_loop_iteration_limit(&mut self, value: u64) {
self.loop_iteration_limit = value;
}
/// Disable loop iteration limit.
#[inline]
pub fn disable_loop_iteration_limit(&mut self) {
self.loop_iteration_limit = u64::MAX;
}
/// Get max stack size.
#[inline]
#[must_use]
pub const fn stack_size_limit(&self) -> usize {
self.stack_size_limit
}
/// Set max stack size before an error is thrown.
#[inline]
pub fn set_stack_size_limit(&mut self, value: usize) {
self.stack_size_limit = value;
}
}

39
boa_engine/src/vm/tests.rs

@ -1,4 +1,4 @@
use crate::{builtins::error::ErrorKind, run_test_actions, JsValue, TestAction}; use crate::{run_test_actions, JsNativeErrorKind, JsValue, TestAction};
use indoc::indoc; use indoc::indoc;
#[test] #[test]
@ -190,7 +190,7 @@ fn super_call_constructor_null() {
} }
new A(); new A();
"#}, "#},
ErrorKind::Type, JsNativeErrorKind::Type,
"super constructor object must be constructor", "super constructor object must be constructor",
)]); )]);
} }
@ -237,3 +237,38 @@ fn order_of_execution_in_assigment_with_comma_expressions() {
"1234", "1234",
)]); )]);
} }
#[test]
fn loop_runtime_limit() {
run_test_actions([
TestAction::assert_eq(
indoc! {r#"
for (let i = 0; i < 20; ++i) { }
"#},
JsValue::undefined(),
),
TestAction::inspect_context(|context| {
context.runtime_limits_mut().set_loop_iteration_limit(10);
}),
TestAction::assert_native_error(
indoc! {r#"
for (let i = 0; i < 20; ++i) { }
"#},
JsNativeErrorKind::RuntimeLimit,
"max loop iteration limit 10 exceeded",
),
TestAction::assert_eq(
indoc! {r#"
for (let i = 0; i < 10; ++i) { }
"#},
JsValue::undefined(),
),
TestAction::assert_native_error(
indoc! {r#"
while (1) { }
"#},
JsNativeErrorKind::RuntimeLimit,
"max loop iteration limit 10 exceeded",
),
]);
}

47
boa_examples/src/bin/runtime_limits.rs

@ -0,0 +1,47 @@
use boa_engine::{Context, Source};
fn main() {
// Create the JavaScript context.
let mut context = Context::default();
// Set the context's runtime limit on loops to 10 iterations.
context.runtime_limits_mut().set_loop_iteration_limit(10);
// The code below iterates 5 times, so no error is thrown.
let result = context.eval_script(Source::from_bytes(
r"
for (let i = 0; i < 5; ++i) { }
",
));
result.expect("no error should be thrown");
// Here we exceed the limit by 1 iteration and a `RuntimeLimit` error is thrown.
//
// This error cannot be caught in JavaScript it propagates to rust caller.
let result = context.eval_script(Source::from_bytes(
r"
try {
for (let i = 0; i < 11; ++i) { }
} catch (e) {
}
",
));
result.expect_err("should have throw an error");
// Preventing an infinity loops
let result = context.eval_script(Source::from_bytes(
r"
while (true) { }
",
));
result.expect_err("should have throw an error");
// The limit applies to all types of loops.
let result = context.eval_script(Source::from_bytes(
r"
for (let e of [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) { }
",
));
result.expect_err("should have throw an error");
}

19
docs/boa_object.md

@ -173,7 +173,7 @@ $boa.optimizer.constantFolding // true
### Getter & Setter `$boa.optimizer.statistics` ### Getter & Setter `$boa.optimizer.statistics`
This is and accessor property on the module, its getter returns `true` if enabled or `false` otherwise. This is an accessor property on the module, its getter returns `true` if enabled or `false` otherwise.
Its setter can be used to enable/disable optimization statistics, which are printed to `stdout`. Its setter can be used to enable/disable optimization statistics, which are printed to `stdout`.
```JavaScript ```JavaScript
@ -190,7 +190,7 @@ Optimizer {
## Module `$boa.realm` ## Module `$boa.realm`
This modules contains realm utilities to test cross-realm behaviour. This module contains realm utilities to test cross-realm behaviour.
### `$boa.realm.create` ### `$boa.realm.create`
@ -240,3 +240,18 @@ $boa.shape.same(o1, o2) // true
o2.y = 200 o2.y = 200
$boa.shape.same(o1, o2) // false $boa.shape.same(o1, o2) // false
``` ```
## Module `$boa.limits`
This module contains utilities for changing runtime limits.
### Getter & Setter `$boa.limits.loop`
This is an accessor property on the module, its getter returns the loop iteration limit before an error is thrown.
Its setter can be used to set the loop iteration limit.
```javascript
$boa.limits.loop = 10;
while (true) {} // RuntimeLimit: max loop iteration limit 10 exceeded
```

Loading…
Cancel
Save