From 6eff7ca00eb26d21ea8508a76c2f55e21b23c41c Mon Sep 17 00:00:00 2001 From: raskad <32105367+raskad@users.noreply.github.com> Date: Wed, 9 Nov 2022 19:51:47 +0000 Subject: [PATCH 01/11] Handle `__proto__` fields in object literals (#2423) This Pull Request changes the following: - Handle `__proto__` fields in object literals --- boa_engine/src/builtins/json/mod.rs | 7 ++- boa_engine/src/bytecompiler/function.rs | 1 + boa_engine/src/bytecompiler/mod.rs | 17 +++++-- boa_engine/src/context/mod.rs | 17 ++++++- boa_engine/src/vm/code_block.rs | 1 + boa_engine/src/vm/opcode/mod.rs | 7 +++ boa_engine/src/vm/opcode/set/mod.rs | 2 + boa_engine/src/vm/opcode/set/prototype.rs | 36 +++++++++++++++ boa_interner/src/sym.rs | 8 +++- boa_parser/src/parser/cursor/mod.rs | 16 +++++++ .../primary/object_initializer/mod.rs | 46 +++++++++++++++---- .../primary/object_initializer/tests.rs | 9 ++-- boa_parser/src/parser/mod.rs | 9 ++++ 13 files changed, 154 insertions(+), 22 deletions(-) create mode 100644 boa_engine/src/vm/opcode/set/prototype.rs diff --git a/boa_engine/src/builtins/json/mod.rs b/boa_engine/src/builtins/json/mod.rs index 34a864838f9..f43a0d6b00a 100644 --- a/boa_engine/src/builtins/json/mod.rs +++ b/boa_engine/src/builtins/json/mod.rs @@ -30,6 +30,7 @@ use crate::{ value::IntegerOrInfinity, Context, JsResult, JsString, JsValue, }; +use boa_parser::Parser; use boa_profiler::Profiler; use tap::{Conv, Pipe}; @@ -194,7 +195,11 @@ impl Json { // 8. NOTE: The PropertyDefinitionEvaluation semantics defined in 13.2.5.5 have special handling for the above evaluation. // 9. Let unfiltered be completion.[[Value]]. // 10. Assert: unfiltered is either a String, Number, Boolean, Null, or an Object that is defined by either an ArrayLiteral or an ObjectLiteral. - let unfiltered = context.eval(script_string.as_bytes())?; + let mut parser = Parser::new(script_string.as_bytes()); + parser.set_json_parse(); + let statement_list = parser.parse_all(context.interner_mut())?; + let code_block = context.compile_json_parse(&statement_list)?; + let unfiltered = context.execute(code_block)?; // 11. If IsCallable(reviver) is true, then if let Some(obj) = args.get_or_undefined(1).as_callable() { diff --git a/boa_engine/src/bytecompiler/function.rs b/boa_engine/src/bytecompiler/function.rs index 815ec309521..9f4a385c378 100644 --- a/boa_engine/src/bytecompiler/function.rs +++ b/boa_engine/src/bytecompiler/function.rs @@ -107,6 +107,7 @@ impl FunctionCompiler { bindings_map: FxHashMap::default(), jump_info: Vec::new(), in_async_generator: self.generator && self.r#async, + json_parse: false, context, }; diff --git a/boa_engine/src/bytecompiler/mod.rs b/boa_engine/src/bytecompiler/mod.rs index 081a6b2259a..33dec83bbc2 100644 --- a/boa_engine/src/bytecompiler/mod.rs +++ b/boa_engine/src/bytecompiler/mod.rs @@ -234,6 +234,7 @@ pub struct ByteCompiler<'b> { bindings_map: FxHashMap, jump_info: Vec, in_async_generator: bool, + json_parse: bool, context: &'b mut Context, } @@ -242,7 +243,7 @@ impl<'b> ByteCompiler<'b> { const DUMMY_ADDRESS: u32 = u32::MAX; #[inline] - pub fn new(name: Sym, strict: bool, context: &'b mut Context) -> Self { + pub fn new(name: Sym, strict: bool, json_parse: bool, context: &'b mut Context) -> Self { Self { code_block: CodeBlock::new(name, 0, strict), literals_map: FxHashMap::default(), @@ -250,6 +251,7 @@ impl<'b> ByteCompiler<'b> { bindings_map: FxHashMap::default(), jump_info: Vec::new(), in_async_generator: false, + json_parse, context, } } @@ -1200,13 +1202,18 @@ impl<'b> ByteCompiler<'b> { match property { PropertyDefinition::IdentifierReference(ident) => { let index = self.get_or_insert_name(*ident); + self.access_get(Access::Variable { name: *ident }, true)?; self.emit(Opcode::DefineOwnPropertyByName, &[index]); } PropertyDefinition::Property(name, expr) => match name { PropertyName::Literal(name) => { self.compile_expr(expr, true)?; let index = self.get_or_insert_name((*name).into()); - self.emit(Opcode::DefineOwnPropertyByName, &[index]); + if *name == Sym::__PROTO__ && !self.json_parse { + self.emit_opcode(Opcode::SetPrototype); + } else { + self.emit(Opcode::DefineOwnPropertyByName, &[index]); + } } PropertyName::Computed(name_node) => { self.compile_expr(name_node, true)?; @@ -3225,6 +3232,7 @@ impl<'b> ByteCompiler<'b> { bindings_map: FxHashMap::default(), jump_info: Vec::new(), in_async_generator: false, + json_parse: self.json_parse, context: self.context, }; compiler.context.push_compile_time_environment(true); @@ -3467,6 +3475,7 @@ impl<'b> ByteCompiler<'b> { bindings_map: FxHashMap::default(), jump_info: Vec::new(), in_async_generator: false, + json_parse: self.json_parse, context: self.context, }; field_compiler.context.push_compile_time_environment(true); @@ -3498,6 +3507,7 @@ impl<'b> ByteCompiler<'b> { bindings_map: FxHashMap::default(), jump_info: Vec::new(), in_async_generator: false, + json_parse: self.json_parse, context: self.context, }; field_compiler.context.push_compile_time_environment(true); @@ -3554,7 +3564,8 @@ impl<'b> ByteCompiler<'b> { } ClassElement::StaticBlock(statement_list) => { self.emit_opcode(Opcode::Dup); - let mut compiler = ByteCompiler::new(Sym::EMPTY_STRING, true, self.context); + let mut compiler = + ByteCompiler::new(Sym::EMPTY_STRING, true, false, self.context); compiler.context.push_compile_time_environment(true); compiler.create_decls(statement_list, false); compiler.compile_statement_list(statement_list, false, false)?; diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index f5531cdf0a7..47cc742b3d1 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -470,7 +470,20 @@ impl Context { #[inline] pub fn compile(&mut self, statement_list: &StatementList) -> JsResult> { let _timer = Profiler::global().start_event("Compilation", "Main"); - let mut compiler = ByteCompiler::new(Sym::MAIN, statement_list.strict(), self); + let mut compiler = ByteCompiler::new(Sym::MAIN, statement_list.strict(), false, self); + compiler.create_decls(statement_list, false); + compiler.compile_statement_list(statement_list, true, false)?; + Ok(Gc::new(compiler.finish())) + } + + /// Compile the AST into a `CodeBlock` ready to be executed by the VM in a `JSON.parse` context. + #[inline] + pub fn compile_json_parse( + &mut self, + statement_list: &StatementList, + ) -> JsResult> { + let _timer = Profiler::global().start_event("Compilation", "Main"); + let mut compiler = ByteCompiler::new(Sym::MAIN, statement_list.strict(), true, self); compiler.create_decls(statement_list, false); compiler.compile_statement_list(statement_list, true, false)?; Ok(Gc::new(compiler.finish())) @@ -484,7 +497,7 @@ impl Context { strict: bool, ) -> JsResult> { let _timer = Profiler::global().start_event("Compilation", "Main"); - let mut compiler = ByteCompiler::new(Sym::MAIN, statement_list.strict(), self); + let mut compiler = ByteCompiler::new(Sym::MAIN, statement_list.strict(), false, self); compiler.compile_statement_list_with_new_declarative(statement_list, true, strict)?; Ok(Gc::new(compiler.finish())) } diff --git a/boa_engine/src/vm/code_block.rs b/boa_engine/src/vm/code_block.rs index c23c75cc046..a8444e20725 100644 --- a/boa_engine/src/vm/code_block.rs +++ b/boa_engine/src/vm/code_block.rs @@ -393,6 +393,7 @@ impl CodeBlock { | Opcode::NewSpread | Opcode::SuperCallSpread | Opcode::ForAwaitOfLoopIterate + | Opcode::SetPrototype | Opcode::Nop => String::new(), } } diff --git a/boa_engine/src/vm/opcode/mod.rs b/boa_engine/src/vm/opcode/mod.rs index 8d8dc6f5f9e..2031d81252c 100644 --- a/boa_engine/src/vm/opcode/mod.rs +++ b/boa_engine/src/vm/opcode/mod.rs @@ -344,6 +344,13 @@ generate_impl! { /// Stack: home, function **=>** home, function SetHomeObject, + /// Set the prototype of an object if the value is an object or null. + /// + /// Operands: + /// + /// Stack: object, value **=>** + SetPrototype, + /// Push an empty array value on the stack. /// /// Operands: diff --git a/boa_engine/src/vm/opcode/set/mod.rs b/boa_engine/src/vm/opcode/set/mod.rs index 8e7b8edcdac..b7be9e79d4c 100644 --- a/boa_engine/src/vm/opcode/set/mod.rs +++ b/boa_engine/src/vm/opcode/set/mod.rs @@ -3,9 +3,11 @@ pub(crate) mod home_object; pub(crate) mod name; pub(crate) mod private; pub(crate) mod property; +pub(crate) mod prototype; pub(crate) use class_prototype::*; pub(crate) use home_object::*; pub(crate) use name::*; pub(crate) use private::*; pub(crate) use property::*; +pub(crate) use prototype::*; diff --git a/boa_engine/src/vm/opcode/set/prototype.rs b/boa_engine/src/vm/opcode/set/prototype.rs new file mode 100644 index 00000000000..d500b916f76 --- /dev/null +++ b/boa_engine/src/vm/opcode/set/prototype.rs @@ -0,0 +1,36 @@ +use crate::{ + vm::{opcode::Operation, ShouldExit}, + Context, JsResult, +}; + +/// `SetPrototype` implements the Opcode Operation for `Opcode::SetPrototype` +/// +/// Operation: +/// - Sets the prototype of an object. +#[derive(Debug, Clone, Copy)] +pub(crate) struct SetPrototype; + +impl Operation for SetPrototype { + const NAME: &'static str = "SetPrototype"; + const INSTRUCTION: &'static str = "INST - SetPrototype"; + + fn execute(context: &mut Context) -> JsResult { + let value = context.vm.pop(); + let object = context.vm.pop(); + + let prototype = if let Some(prototype) = value.as_object() { + Some(prototype.clone()) + } else if value.is_null() { + None + } else { + return Ok(ShouldExit::False); + }; + + let object = object.as_object().expect("object is not an object"); + object + .__set_prototype_of__(prototype, context) + .expect("cannot fail per spec"); + + Ok(ShouldExit::False) + } +} diff --git a/boa_interner/src/sym.rs b/boa_interner/src/sym.rs index f77c8739625..8cf8385bef0 100644 --- a/boa_interner/src/sym.rs +++ b/boa_interner/src/sym.rs @@ -104,6 +104,9 @@ impl Sym { /// Symbol for the `"target"` string. pub const TARGET: Self = unsafe { Self::new_unchecked(28) }; + /// Symbol for the `"__proto__"` string. + pub const __PROTO__: Self = unsafe { Self::new_unchecked(29) }; + /// Creates a new [`Sym`] from the provided `value`, or returns `None` if `index` is zero. #[inline] pub(super) fn new(value: usize) -> Option { @@ -134,7 +137,7 @@ impl Sym { } macro_rules! create_static_strings { - ( $( $s:literal ),+ ) => { + ( $( $s:literal ),+$(,)? ) => { /// Ordered set of commonly used static `UTF-8` strings. /// /// # Note @@ -193,5 +196,6 @@ create_static_strings! { "false", "async", "of", - "target" + "target", + "__proto__", } diff --git a/boa_parser/src/parser/cursor/mod.rs b/boa_parser/src/parser/cursor/mod.rs index 4383c2d5123..8f260c84b84 100644 --- a/boa_parser/src/parser/cursor/mod.rs +++ b/boa_parser/src/parser/cursor/mod.rs @@ -31,6 +31,9 @@ pub(super) struct Cursor { /// Tracks if the cursor is in a arrow function declaration. arrow: bool, + + /// Indicate if the cursor is used in `JSON.parse`. + json_parse: bool, } impl Cursor @@ -44,6 +47,7 @@ where buffered_lexer: Lexer::new(reader).into(), private_environments_stack: Vec::new(), arrow: false, + json_parse: false, } } @@ -126,6 +130,18 @@ where self.arrow = arrow; } + /// Returns if the cursor is currently used in `JSON.parse`. + #[inline] + pub(super) fn json_parse(&self) -> bool { + self.json_parse + } + + /// Set if the cursor is currently used in `JSON.parse`. + #[inline] + pub(super) fn set_json_parse(&mut self, json_parse: bool) { + self.json_parse = json_parse; + } + /// Push a new private environment. #[inline] pub(super) fn push_private_environment(&mut self) { diff --git a/boa_parser/src/parser/expression/primary/object_initializer/mod.rs b/boa_parser/src/parser/expression/primary/object_initializer/mod.rs index 9b4febb8f8f..cf975a74c8a 100644 --- a/boa_parser/src/parser/expression/primary/object_initializer/mod.rs +++ b/boa_parser/src/parser/expression/primary/object_initializer/mod.rs @@ -75,15 +75,34 @@ where let _timer = Profiler::global().start_event("ObjectLiteral", "Parsing"); let mut elements = Vec::new(); + let mut has_proto = false; + let mut duplicate_proto_position = None; + loop { if cursor.next_if(Punctuator::CloseBlock, interner)?.is_some() { break; } - elements.push( - PropertyDefinition::new(self.allow_yield, self.allow_await) - .parse(cursor, interner)?, - ); + let position = cursor.peek(0, interner).or_abrupt()?.span().start(); + + let property = PropertyDefinition::new(self.allow_yield, self.allow_await) + .parse(cursor, interner)?; + + if matches!( + property, + property::PropertyDefinition::Property( + property::PropertyName::Literal(Sym::__PROTO__), + _ + ) + ) { + if has_proto && duplicate_proto_position.is_none() { + duplicate_proto_position = Some(position); + } else { + has_proto = true; + } + } + + elements.push(property); if cursor.next_if(Punctuator::CloseBlock, interner)?.is_some() { break; @@ -100,6 +119,20 @@ where } } + if let Some(position) = duplicate_proto_position { + if !cursor.json_parse() + && match cursor.peek(0, interner)? { + Some(token) => token.kind() != &TokenKind::Punctuator(Punctuator::Assign), + None => true, + } + { + return Err(Error::general( + "Duplicate __proto__ fields are not allowed in object literals.", + position, + )); + } + } + Ok(literal::ObjectLiteral::from(elements)) } } @@ -143,10 +176,7 @@ where TokenKind::Punctuator(Punctuator::CloseBlock | Punctuator::Comma) => { let ident = IdentifierReference::new(self.allow_yield, self.allow_await) .parse(cursor, interner)?; - return Ok(property::PropertyDefinition::Property( - ident.sym().into(), - ident.into(), - )); + return Ok(property::PropertyDefinition::IdentifierReference(ident)); } TokenKind::Punctuator(Punctuator::Assign) => { return CoverInitializedName::new(self.allow_yield, self.allow_await) diff --git a/boa_parser/src/parser/expression/primary/object_initializer/tests.rs b/boa_parser/src/parser/expression/primary/object_initializer/tests.rs index 40ff65d1bb6..4f0bf2cab9f 100644 --- a/boa_parser/src/parser/expression/primary/object_initializer/tests.rs +++ b/boa_parser/src/parser/expression/primary/object_initializer/tests.rs @@ -284,9 +284,8 @@ fn check_object_short_function_set() { fn check_object_shorthand_property_names() { let interner = &mut Interner::default(); - let object_properties = vec![PropertyDefinition::Property( + let object_properties = vec![PropertyDefinition::IdentifierReference( interner.get_or_intern_static("a", utf16!("a")).into(), - Identifier::new(interner.get_or_intern_static("a", utf16!("a"))).into(), )]; check_parser( @@ -322,13 +321,11 @@ fn check_object_shorthand_multiple_properties() { let interner = &mut Interner::default(); let object_properties = vec![ - PropertyDefinition::Property( + PropertyDefinition::IdentifierReference( interner.get_or_intern_static("a", utf16!("a")).into(), - Identifier::new(interner.get_or_intern_static("a", utf16!("a"))).into(), ), - PropertyDefinition::Property( + PropertyDefinition::IdentifierReference( interner.get_or_intern_static("b", utf16!("b")).into(), - Identifier::new(interner.get_or_intern_static("b", utf16!("b"))).into(), ), ]; diff --git a/boa_parser/src/parser/mod.rs b/boa_parser/src/parser/mod.rs index 1f20d07af93..9a884380a79 100644 --- a/boa_parser/src/parser/mod.rs +++ b/boa_parser/src/parser/mod.rs @@ -139,6 +139,15 @@ impl Parser { self.cursor.set_strict_mode(true); } + /// Set the parser strict mode to true. + #[inline] + pub fn set_json_parse(&mut self) + where + R: Read, + { + self.cursor.set_json_parse(true); + } + /// Parse the full input as a [ECMAScript Script][spec] into the boa AST representation. /// The resulting `StatementList` can be compiled into boa bytecode and executed in the boa vm. /// From 2362f7353c015b530ba74616485604e74fc644a4 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 10 Nov 2022 14:39:45 +0000 Subject: [PATCH 02/11] Bump webpack from 5.74.0 to 5.75.0 (#2427) Bumps [webpack](https://github.com/webpack/webpack) from 5.74.0 to 5.75.0.
Release notes

Sourced from webpack's releases.

v5.75.0

Bugfixes

  • experiments.* normalize to false when opt-out
  • avoid NaN%
  • show the correct error when using a conflicting chunk name in code
  • HMR code tests existance of window before trying to access it
  • fix eval-nosources-* actually exclude sources
  • fix race condition where no module is returned from processing module
  • fix position of standalong semicolon in runtime code

Features

  • add support for @import to extenal CSS when using experimental CSS in node
  • add i64 support to the deprecated WASM implementation

Developer Experience

  • expose EnableWasmLoadingPlugin
  • add more typings
  • generate getters instead of readonly properties in typings to allow overriding them
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=webpack&package-manager=npm_and_yarn&previous-version=5.74.0&new-version=5.75.0)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- package-lock.json | 14 +++++++------- package.json | 2 +- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 5133b487a56..2ae907935ac 100644 --- a/package-lock.json +++ b/package-lock.json @@ -19,7 +19,7 @@ "prettier": "^2.7.1", "style-loader": "^3.3.1", "terser-webpack-plugin": "^5.3.6", - "webpack": "^5.74.0", + "webpack": "^5.75.0", "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.11.1" } @@ -4300,9 +4300,9 @@ } }, "node_modules/webpack": { - "version": "5.74.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz", - "integrity": "sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==", + "version": "5.75.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", + "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==", "dev": true, "dependencies": { "@types/eslint-scope": "^3.7.3", @@ -7886,9 +7886,9 @@ } }, "webpack": { - "version": "5.74.0", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.74.0.tgz", - "integrity": "sha512-A2InDwnhhGN4LYctJj6M1JEaGL7Luj6LOmyBHjcI8529cm5p6VXiTIW2sn6ffvEAKmveLzvu4jrihwXtPojlAA==", + "version": "5.75.0", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.75.0.tgz", + "integrity": "sha512-piaIaoVJlqMsPtX/+3KTTO6jfvrSYgauFVdt8cr9LTHKmcq/AMd4mhzsiP7ZF/PGRNPGA8336jldh9l2Kt2ogQ==", "dev": true, "requires": { "@types/eslint-scope": "^3.7.3", diff --git a/package.json b/package.json index 3d50b2c6e2d..9179820ca4d 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "prettier": "^2.7.1", "style-loader": "^3.3.1", "terser-webpack-plugin": "^5.3.6", - "webpack": "^5.74.0", + "webpack": "^5.75.0", "webpack-cli": "^4.10.0", "webpack-dev-server": "^4.11.1" }, From fdac8ece5b689bedf1253c805c81351a9470423f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Juli=C3=A1n=20Espina?= Date: Fri, 11 Nov 2022 17:21:07 +0000 Subject: [PATCH 03/11] Fix some Date tests (#2431) Found some tests on the `Date` builtin that were failing for incorrect length attributes and missing checks. --- boa_engine/src/builtins/date/mod.rs | 48 +++++++++++++---------------- boa_engine/src/value/integer.rs | 23 ++++++++++++++ boa_engine/src/value/mod.rs | 19 ++---------- 3 files changed, 47 insertions(+), 43 deletions(-) diff --git a/boa_engine/src/builtins/date/mod.rs b/boa_engine/src/builtins/date/mod.rs index b23877a7fc3..2319ce053be 100644 --- a/boa_engine/src/builtins/date/mod.rs +++ b/boa_engine/src/builtins/date/mod.rs @@ -12,7 +12,7 @@ use crate::{ }, string::utf16, symbol::WellKnownSymbols, - value::{JsValue, PreferredType}, + value::{IntegerOrInfinity, JsValue, PreferredType}, Context, JsError, JsResult, }; use boa_profiler::Profiler; @@ -121,9 +121,9 @@ impl BuiltIn for Date { .method(Self::to_gmt_string, "toGMTString", 0) .method(Self::to_iso_string, "toISOString", 0) .method(Self::to_json, "toJSON", 1) - .method(Self::to_locale_date_string, "toLocaleDateString", 2) - .method(Self::to_locale_string, "toLocaleString", 2) - .method(Self::to_locale_time_string, "toLocaleTimeString", 2) + .method(Self::to_locale_date_string, "toLocaleDateString", 0) + .method(Self::to_locale_string, "toLocaleString", 0) + .method(Self::to_locale_time_string, "toLocaleTimeString", 0) .method(Self::to_string, "toString", 0) .method(Self::to_time_string, "toTimeString", 0) .method(Self::to_utc_string, "toUTCString", 0) @@ -152,11 +152,16 @@ impl Date { /// /// [spec]: https://tc39.es/ecma262/#sec-timeclip #[inline] - pub fn time_clip(time: f64) -> Option { - if time.abs() > 8.64e15 { - None - } else { - Some(time) + pub fn time_clip(time: f64) -> Option { + // 1. If time is not finite, return NaN. + // 2. If abs(ℝ(time)) > 8.64 × 1015, return NaN. + // 3. Return 𝔽(! ToIntegerOrInfinity(time)). + if time.is_nan() { + return None; + } + match IntegerOrInfinity::from(time) { + IntegerOrInfinity::Integer(i) if i.abs() <= 864i64 * 10i64.pow(13) => Some(i), + _ => None, } } @@ -1329,26 +1334,17 @@ impl Date { this_time_value(this)?; // 2. Let t be ? ToNumber(time). - let t = if let Some(t) = args.get(0) { - let t = t.to_number(context)?; - let seconds = (t / 1_000f64) as i64; - let nanoseconds = ((t % 1_000f64) * 1_000_000f64) as u32; - Self( - ignore_ambiguity(Local.timestamp_opt(seconds, nanoseconds)) - .map(|dt| dt.naive_utc()), - ) - } else { - Self(None) - }; + let t = args.get_or_undefined(0).to_number(context)?; // 3. Let v be TimeClip(t). - let v = Self::get_time(this, args, context)?; + let v_int = Self::time_clip(t); + let v = v_int.map(|t| Local.timestamp_millis(t).naive_utc()); // 4. Set the [[DateValue]] internal slot of this Date object to v. - this.set_data(ObjectData::date(t)); + this.set_data(ObjectData::date(Date(v))); // 5. Return v. - Ok(v) + Ok(v_int.map_or(JsValue::Rational(f64::NAN), JsValue::from)) } /// `Date.prototype.setUTCDate()` @@ -1728,7 +1724,7 @@ impl Date { _args: &[JsValue], context: &mut Context, ) -> JsResult { - Self::to_utc_string(this, &[JsValue::Null], context) + Self::to_utc_string(this, &[], context) } /// `Date.prototype.toISOString()` @@ -1903,13 +1899,13 @@ impl Date { pub fn to_utc_string( this: &JsValue, _args: &[JsValue], - context: &mut Context, + _context: &mut Context, ) -> JsResult { if let Some(t) = this_time_value(this)?.0 { let utc_string = t.format("%a, %d %b %Y %H:%M:%S GMT").to_string(); Ok(JsValue::new(utc_string)) } else { - Ok(context.construct_error("Invalid time value")) + Ok(js_string!("Invalid Date").into()) } } diff --git a/boa_engine/src/value/integer.rs b/boa_engine/src/value/integer.rs index d4a1b281331..2cebeeadb31 100644 --- a/boa_engine/src/value/integer.rs +++ b/boa_engine/src/value/integer.rs @@ -30,6 +30,29 @@ impl IntegerOrInfinity { } } +impl From for IntegerOrInfinity { + fn from(number: f64) -> Self { + // `ToIntegerOrInfinity ( argument )` + if number.is_nan() || number == 0.0 { + // 2. If number is NaN, +0𝔽, or -0𝔽, return 0. + Self::Integer(0) + } else if number == f64::INFINITY { + // 3. If number is +∞𝔽, return +∞. + Self::PositiveInfinity + } else if number == f64::NEG_INFINITY { + // 4. If number is -∞𝔽, return -∞. + Self::NegativeInfinity + } else { + // 5. Let integer be floor(abs(ℝ(number))). + // 6. If number < +0𝔽, set integer to -integer. + let integer = number.abs().floor().copysign(number) as i64; + + // 7. Return integer. + Self::Integer(integer) + } + } +} + impl PartialEq for IntegerOrInfinity { fn eq(&self, other: &i64) -> bool { match self { diff --git a/boa_engine/src/value/mod.rs b/boa_engine/src/value/mod.rs index a5df0b672cf..4b4b46713b7 100644 --- a/boa_engine/src/value/mod.rs +++ b/boa_engine/src/value/mod.rs @@ -865,23 +865,8 @@ impl JsValue { // 1. Let number be ? ToNumber(argument). let number = self.to_number(context)?; - if number.is_nan() || number == 0.0 { - // 2. If number is NaN, +0𝔽, or -0𝔽, return 0. - Ok(IntegerOrInfinity::Integer(0)) - } else if number == f64::INFINITY { - // 3. If number is +∞𝔽, return +∞. - Ok(IntegerOrInfinity::PositiveInfinity) - } else if number == f64::NEG_INFINITY { - // 4. If number is -∞𝔽, return -∞. - Ok(IntegerOrInfinity::NegativeInfinity) - } else { - // 5. Let integer be floor(abs(ℝ(number))). - // 6. If number < +0𝔽, set integer to -integer. - let integer = number.abs().floor().copysign(number) as i64; - - // 7. Return integer. - Ok(IntegerOrInfinity::Integer(integer)) - } + // Continues on `IntegerOrInfinity::from::` + Ok(IntegerOrInfinity::from(number)) } /// Converts a value to a double precision floating point. From c2dd0271d0b1583d37d7b9d63907851ae319e4d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jo=C3=A3o=20Borges?= Date: Mon, 14 Nov 2022 06:39:24 +0000 Subject: [PATCH 04/11] Switch tarpaulin to llvm engine (#2432) Trying this to see if it makes coverage numbers more accurate --- .github/workflows/rust.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 58c181022a8..99ea7b3223d 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -27,7 +27,7 @@ jobs: - name: Run cargo-tarpaulin uses: actions-rs/tarpaulin@v0.1 with: - args: --features intl --ignore-tests + args: --features intl --ignore-tests --engine llvm - name: Upload to codecov.io uses: codecov/codecov-action@v3 From e8e95f2311ac0bcc4e893b8e10f225d882a4e6cd Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Nov 2022 08:50:46 +0000 Subject: [PATCH 05/11] Bump test262 from `f6c48f3` to `1d5dc6b` (#2437) Bumps [test262](https://github.com/tc39/test262) from `f6c48f3` to `1d5dc6b`.
Commits
  • 1d5dc6b Remove tests involving the Emoji_Test property
  • e04de94 Line Terminator test description corrections
  • 0593463 Add missed changes for Symbols as WeakMap keys proposal.
  • See full diff in compare view

Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- test262 | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test262 b/test262 index f6c48f333e7..1d5dc6b577a 160000 --- a/test262 +++ b/test262 @@ -1 +1 @@ -Subproject commit f6c48f333e7d16bf11b30cd29b4ea9ac0354f142 +Subproject commit 1d5dc6b577ae6728c087f7d6143c3a5ccf88bcff From fcab8051d4a843855e4c9f99a6394823fa4e4473 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Nov 2022 12:54:54 +0000 Subject: [PATCH 06/11] Bump css-loader from 6.7.1 to 6.7.2 (#2434) Bumps [css-loader](https://github.com/webpack-contrib/css-loader) from 6.7.1 to 6.7.2.
Release notes

Sourced from css-loader's releases.

v6.7.2

6.7.2 (2022-11-13)

Bug Fixes

  • css modules generation with inline syntax (#1480) (2f4c273)
Changelog

Sourced from css-loader's changelog.

6.7.2 (2022-11-13)

Bug Fixes

  • css modules generation with inline syntax (#1480) (2f4c273)
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=css-loader&package-manager=npm_and_yarn&previous-version=6.7.1&new-version=6.7.2)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- package-lock.json | 46 +++++++++++++++++++++++----------------------- package.json | 2 +- 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/package-lock.json b/package-lock.json index 2ae907935ac..a6a69cff3e2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -12,7 +12,7 @@ "bootstrap": "^5.2.2", "clean-webpack-plugin": "^4.0.0", "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.7.1", + "css-loader": "^6.7.2", "file-loader": "^6.2.0", "html-webpack-plugin": "^5.5.0", "monaco-editor-webpack-plugin": "^7.0.1", @@ -1167,19 +1167,19 @@ } }, "node_modules/css-loader": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz", - "integrity": "sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.2.tgz", + "integrity": "sha512-oqGbbVcBJkm8QwmnNzrFrWTnudnRZC+1eXikLJl0n4ljcfotgRifpg2a1lKy8jTrc4/d9A/ap1GFq1jDKG7J+Q==", "dev": true, "dependencies": { "icss-utils": "^5.1.0", - "postcss": "^8.4.7", + "postcss": "^8.4.18", "postcss-modules-extract-imports": "^3.0.0", "postcss-modules-local-by-default": "^4.0.0", "postcss-modules-scope": "^3.0.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", - "semver": "^7.3.5" + "semver": "^7.3.8" }, "engines": { "node": ">= 12.13.0" @@ -3165,9 +3165,9 @@ } }, "node_modules/postcss": { - "version": "8.4.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", - "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "version": "8.4.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz", + "integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==", "dev": true, "funding": [ { @@ -3636,9 +3636,9 @@ } }, "node_modules/semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dev": true, "dependencies": { "lru-cache": "^6.0.0" @@ -5583,19 +5583,19 @@ } }, "css-loader": { - "version": "6.7.1", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.1.tgz", - "integrity": "sha512-yB5CNFa14MbPJcomwNh3wLThtkZgcNyI2bNMRt8iE5Z8Vwl7f8vQXFAzn2HDOJvtDq2NTZBUGMSUNNyrv3/+cw==", + "version": "6.7.2", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.7.2.tgz", + "integrity": "sha512-oqGbbVcBJkm8QwmnNzrFrWTnudnRZC+1eXikLJl0n4ljcfotgRifpg2a1lKy8jTrc4/d9A/ap1GFq1jDKG7J+Q==", "dev": true, "requires": { "icss-utils": "^5.1.0", - "postcss": "^8.4.7", + "postcss": "^8.4.18", "postcss-modules-extract-imports": "^3.0.0", "postcss-modules-local-by-default": "^4.0.0", "postcss-modules-scope": "^3.0.0", "postcss-modules-values": "^4.0.0", "postcss-value-parser": "^4.2.0", - "semver": "^7.3.5" + "semver": "^7.3.8" } }, "css-select": { @@ -7073,9 +7073,9 @@ } }, "postcss": { - "version": "8.4.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", - "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "version": "8.4.19", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.19.tgz", + "integrity": "sha512-h+pbPsyhlYj6N2ozBmHhHrs9DzGmbaarbLvWipMRO7RLS+v4onj26MPFXA5OBYFxyqYhUJK456SwDcY9H2/zsA==", "dev": true, "requires": { "nanoid": "^3.3.4", @@ -7387,9 +7387,9 @@ } }, "semver": { - "version": "7.3.7", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.7.tgz", - "integrity": "sha512-QlYTucUYOews+WeEujDoEGziz4K6c47V/Bd+LjSSYcA94p+DmINdf7ncaUinThfvZyu13lN9OY1XDxt8C0Tw0g==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.3.8.tgz", + "integrity": "sha512-NB1ctGL5rlHrPJtFDVIVzTyQylMLu9N9VICA6HSFJo8MCGVTMW6gfpicwKmmK/dAjTOrqu5l63JJOpDSrAis3A==", "dev": true, "requires": { "lru-cache": "^6.0.0" diff --git a/package.json b/package.json index 9179820ca4d..f212f119db6 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,7 @@ "bootstrap": "^5.2.2", "clean-webpack-plugin": "^4.0.0", "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.7.1", + "css-loader": "^6.7.2", "file-loader": "^6.2.0", "html-webpack-plugin": "^5.5.0", "monaco-editor-webpack-plugin": "^7.0.1", From 4493254de7ecf3e0d216a5631802d626a11be4ab Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Nov 2022 13:03:57 +0000 Subject: [PATCH 07/11] Bump chrono from 0.4.22 to 0.4.23 (#2436) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.22 to 0.4.23.
Release notes

Sourced from chrono's releases.

0.4.23 is the next 0.4 release of the popular chrono date and time library for Rust. After the 0.4.20-0.4.22 series that brought chrono back to life after a long hiatus, development has been fairly quiet, allowing us to start planning changes for the 0.5.0 release. As such, we've started deprecating some APIs that are likely to be removed in 0.5. If you have any feedback on these changes, please let us know in the issue tracker!

Deprecations

  • Deprecate methods that have an _opt() alternative (#827)
  • Deprecate usage of the Date<Tz> type (#851)

Features

  • Optimize RFC 3339 (and RFC 2822) encoding (#844, thanks to @​conradludgate)
  • Addition and subtraction with the Days type (#784)
  • Add NaiveDateTime::from_timestamp_millis(_opt) (#818, thanks to @​Pscheidl -- backported in #823)
  • Allow for changing TZ variable and cache it for Local timezone (#853)
  • Add optional support for the arbitrary::Arbitrary trait (#849, thanks to @​greyblake and @​asayers)

Fixes

  • Support tzdb location on AIX (#826)
  • Fix warnings in documentation (#847)

On behalf of @​esheppa and @​djc, thanks to all contributors!

Commits
  • 9e5eb49 Bump version to 0.4.23
  • dc4287a store hash of environment variable
  • 57908e9 allow sharing of the allocated environment variable
  • 84f3c30 move last_changed to the Cache
  • 8bc4139 add bench for Local::now()
  • 22b4d32 Avoid use of deprecated API
  • 77317d5 Deprecate usage of the Date<Tz> type
  • 7ba090d Add TimeZone::with_ymd_and_hms() helper method
  • 03165c8 Move Date::years_since() implementation into NaiveDate
  • 645fca0 chore: apply clippy suggestions for 1.65
  • Additional commits viewable in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=chrono&package-manager=cargo&previous-version=0.4.22&new-version=0.4.23)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- Cargo.lock | 4 ++-- boa_engine/Cargo.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 423428b13ef..b3fa8672403 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -275,9 +275,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" dependencies = [ "iana-time-zone", "js-sys", diff --git a/boa_engine/Cargo.toml b/boa_engine/Cargo.toml index 1db60c0a7f3..2d6fd774ee2 100644 --- a/boa_engine/Cargo.toml +++ b/boa_engine/Cargo.toml @@ -49,7 +49,7 @@ num-integer = "0.1.45" bitflags = "1.3.2" indexmap = "1.9.1" ryu-js = "0.2.2" -chrono = "0.4.22" +chrono = "0.4.23" fast-float = "0.2.0" unicode-normalization = "0.1.22" dyn-clone = "1.0.9" From 41d57f49e2b95e5ab02b8f739c50471d6cae7cea Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 14 Nov 2022 19:16:30 +0000 Subject: [PATCH 08/11] Bump clap from 4.0.22 to 4.0.23 (#2435) Bumps [clap](https://github.com/clap-rs/clap) from 4.0.22 to 4.0.23.
Release notes

Sourced from clap's releases.

v4.0.23

[4.0.23] - 2022-11-11

Fixes

  • Don't panic on reporting invalid-long errors when followed by invalid UTF8
  • (help) Clarified argument to help subcommand
Changelog

Sourced from clap's changelog.

[4.0.23] - 2022-11-11

Fixes

  • Don't panic on reporting invalid-long errors when followed by invalid UTF8
  • (help) Clarified argument to help subcommand
Commits
  • 95144b7 chore: Release
  • 20ecae1 docs: Update changelog
  • e6a3529 Merge pull request #4474 from epage/utf8
  • e9cbed3 fix(parser): Don't panic on invalid UTF-8 values
  • 45d26e0 test(parser): Show UTF8 bug
  • 4d69e56 Merge pull request #4471 from epage/assert
  • ec03972 test(assert): Verify empty positional assert exists
  • 0d27188 Merge pull request #4465 from epage/help
  • 9376a57 fix(help): Clarify that 'help' command accepts multiple
  • See full diff in compare view

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=clap&package-manager=cargo&previous-version=4.0.22&new-version=4.0.23)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- Cargo.lock | 8 ++++---- boa_cli/Cargo.toml | 2 +- boa_tester/Cargo.toml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index b3fa8672403..96f665cea0d 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,7 +85,7 @@ dependencies = [ "boa_engine", "boa_interner", "boa_parser", - "clap 4.0.22", + "clap 4.0.23", "colored", "jemallocator", "phf", @@ -215,7 +215,7 @@ dependencies = [ "boa_gc", "boa_interner", "boa_parser", - "clap 4.0.22", + "clap 4.0.23", "colored", "fxhash", "gc", @@ -329,9 +329,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.0.22" +version = "4.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "91b9970d7505127a162fdaa9b96428d28a479ba78c9ec7550a63a5d9863db682" +checksum = "0eb41c13df48950b20eb4cd0eefa618819469df1bffc49d11e8487c4ba0037e5" dependencies = [ "atty", "bitflags", diff --git a/boa_cli/Cargo.toml b/boa_cli/Cargo.toml index f9f4ea292c5..312e4d05a82 100644 --- a/boa_cli/Cargo.toml +++ b/boa_cli/Cargo.toml @@ -18,7 +18,7 @@ boa_interner.workspace = true boa_parser.workspace = true rustyline = "10.0.0" rustyline-derive = "0.7.0" -clap = { version = "4.0.22", features = ["derive"] } +clap = { version = "4.0.23", features = ["derive"] } serde_json = "1.0.87" colored = "2.0.0" regex = "1.7.0" diff --git a/boa_tester/Cargo.toml b/boa_tester/Cargo.toml index d3f9bc968f3..2d55ff8aeeb 100644 --- a/boa_tester/Cargo.toml +++ b/boa_tester/Cargo.toml @@ -16,7 +16,7 @@ boa_engine = { workspace = true, features = ["intl"] } boa_interner.workspace = true boa_gc.workspace = true boa_parser.workspace = true -clap = { version = "4.0.22", features = ["derive"] } +clap = { version = "4.0.23", features = ["derive"] } serde = { version = "1.0.147", features = ["derive"] } serde_yaml = "0.9.14" serde_json = "1.0.87" From 98e6dd36cb28eec2c7c5447c8dbf71d147469180 Mon Sep 17 00:00:00 2001 From: Kevin Date: Mon, 14 Nov 2022 19:59:51 +0000 Subject: [PATCH 09/11] Boa Gc implementation draft (#2394) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Not sure if anyone else may be working on something more substantial/in-depth, but I thought I'd post this. 😄 The basic rundown is that this is more of an untested (and in some ways naïve) draft than anything else. It builds rather heavily on `rust-gc`, and tries to keep plenty of the core aspects so as to not break anything too much, and also to minimize overarching changes were it to actually be merged at some point. This implementation does add ~~a generational divide (although a little unoptimized) to the heap,~~ a GcAlloc/Collector struct with methods, and an ephemeron implementation that allows for the WeakPair and WeakGc pointers. --- Cargo.lock | 149 +++-- boa_engine/Cargo.toml | 1 - .../src/builtins/async_generator/mod.rs | 6 +- boa_engine/src/builtins/function/mod.rs | 10 +- boa_engine/src/builtins/generator/mod.rs | 4 +- boa_engine/src/builtins/promise/mod.rs | 4 +- boa_engine/src/bytecompiler/mod.rs | 4 +- boa_engine/src/environments/compile.rs | 8 +- boa_engine/src/environments/runtime.rs | 2 +- boa_engine/src/job.rs | 2 +- boa_engine/src/object/jsobject.rs | 14 +- boa_engine/src/realm.rs | 6 +- boa_engine/src/string/mod.rs | 4 +- boa_engine/src/symbol.rs | 4 +- boa_engine/src/vm/code_block.rs | 8 +- boa_examples/Cargo.toml | 1 - boa_gc/Cargo.toml | 3 +- boa_gc/src/cell.rs | 594 ++++++++++++++++++ boa_gc/src/internals/ephemeron_box.rs | 146 +++++ boa_gc/src/internals/gc_box.rs | 195 ++++++ boa_gc/src/internals/mod.rs | 5 + boa_gc/src/lib.rs | 385 +++++++++++- boa_gc/src/pointers/ephemeron.rs | 125 ++++ boa_gc/src/pointers/gc.rs | 275 ++++++++ boa_gc/src/pointers/mod.rs | 9 + boa_gc/src/pointers/weak.rs | 49 ++ boa_gc/src/test/allocation.rs | 31 + boa_gc/src/test/cell.rs | 15 + boa_gc/src/test/mod.rs | 37 ++ boa_gc/src/test/weak.rs | 133 ++++ boa_gc/src/trace.rs | 450 +++++++++++++ boa_macros/Cargo.toml | 3 +- boa_macros/src/lib.rs | 101 +++ boa_tester/Cargo.toml | 1 - boa_tester/src/exec/mod.rs | 6 +- 35 files changed, 2662 insertions(+), 128 deletions(-) create mode 100644 boa_gc/src/cell.rs create mode 100644 boa_gc/src/internals/ephemeron_box.rs create mode 100644 boa_gc/src/internals/gc_box.rs create mode 100644 boa_gc/src/internals/mod.rs create mode 100644 boa_gc/src/pointers/ephemeron.rs create mode 100644 boa_gc/src/pointers/gc.rs create mode 100644 boa_gc/src/pointers/mod.rs create mode 100644 boa_gc/src/pointers/weak.rs create mode 100644 boa_gc/src/test/allocation.rs create mode 100644 boa_gc/src/test/cell.rs create mode 100644 boa_gc/src/test/mod.rs create mode 100644 boa_gc/src/test/weak.rs create mode 100644 boa_gc/src/trace.rs diff --git a/Cargo.lock b/Cargo.lock index 96f665cea0d..1447e5da805 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -112,7 +112,6 @@ dependencies = [ "dyn-clone", "fast-float", "float-cmp", - "gc", "icu_datetime", "icu_locale_canonicalizer", "icu_locid", @@ -148,14 +147,14 @@ dependencies = [ "boa_gc", "boa_interner", "boa_parser", - "gc", ] [[package]] name = "boa_gc" version = "0.16.0" dependencies = [ - "gc", + "boa_macros", + "boa_profiler", "measureme", ] @@ -177,8 +176,10 @@ dependencies = [ name = "boa_macros" version = "0.16.0" dependencies = [ + "proc-macro2", "quote", "syn", + "synstructure", ] [[package]] @@ -218,7 +219,6 @@ dependencies = [ "clap 4.0.23", "colored", "fxhash", - "gc", "once_cell", "rayon", "regex", @@ -245,9 +245,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.11.0" +version = "3.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c1ad822118d20d2c234f427000d5acc36eabe1e29a348c89b63dd60b13f28e5d" +checksum = "572f695136211188308f16ad2ca5c851a712c464060ae6974944458eb83880ba" [[package]] name = "byteorder" @@ -263,9 +263,9 @@ checksum = "37b2a672a2cb129a2e41c10b1224bb368f9f37a2b16b612598138befd7b37eb5" [[package]] name = "cc" -version = "1.0.73" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" [[package]] name = "cfg-if" @@ -317,9 +317,9 @@ dependencies = [ [[package]] name = "clap" -version = "3.2.22" +version = "3.2.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86447ad904c7fb335a790c9d7fe3d0d971dc523b8ccd1561a520de9a85302750" +checksum = "71655c45cb9845d3270c9d6df84ebe72b4dad3c2ba3f7023ad47c144e4e473a5" dependencies = [ "bitflags", "clap_lex 0.2.4", @@ -421,7 +421,7 @@ dependencies = [ "atty", "cast", "ciborium", - "clap 3.2.22", + "clap 3.2.23", "criterion-plot", "itertools", "lazy_static", @@ -492,9 +492,9 @@ dependencies = [ [[package]] name = "cxx" -version = "1.0.78" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f39818dcfc97d45b03953c1292efc4e80954e1583c4aa770bac1383e2310a4" +checksum = "6b7d4e43b25d3c994662706a1d4fcfc32aaa6afd287502c111b237093bb23f3a" dependencies = [ "cc", "cxxbridge-flags", @@ -504,9 +504,9 @@ dependencies = [ [[package]] name = "cxx-build" -version = "1.0.78" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3e580d70777c116df50c390d1211993f62d40302881e54d4b79727acb83d0199" +checksum = "84f8829ddc213e2c1368e51a2564c552b65a8cb6a28f31e576270ac81d5e5827" dependencies = [ "cc", "codespan-reporting", @@ -519,15 +519,15 @@ dependencies = [ [[package]] name = "cxxbridge-flags" -version = "1.0.78" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56a46460b88d1cec95112c8c363f0e2c39afdb237f60583b0b36343bf627ea9c" +checksum = "e72537424b474af1460806647c41d4b6d35d09ef7fe031c5c2fa5766047cc56a" [[package]] name = "cxxbridge-macro" -version = "1.0.78" +version = "1.0.80" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747b608fecf06b0d72d440f27acc99288207324b793be2c17991839f3d4995ea" +checksum = "309e4fb93eed90e1e14bea0da16b209f81813ba9fc7830c20ed151dd7bc0a4d7" dependencies = [ "proc-macro2", "quote", @@ -634,9 +634,9 @@ checksum = "95765f67b4b18863968b4a1bd5bb576f732b29a4a28c7cd84c09fa3e2875f33c" [[package]] name = "fd-lock" -version = "3.0.6" +version = "3.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e11dcc7e4d79a8c89b9ab4c6f5c30b1fc4a83c420792da3542fd31179ed5f517" +checksum = "0c93a581058d957dc4176875aad04f82f81613e6611d64aa1a9c755bdfb16711" dependencies = [ "cfg-if", "rustix", @@ -679,27 +679,6 @@ dependencies = [ "byteorder", ] -[[package]] -name = "gc" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3edaac0f5832202ebc99520cb77c932248010c4645d20be1dc62d6579f5b3752" -dependencies = [ - "gc_derive", -] - -[[package]] -name = "gc_derive" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60df8444f094ff7885631d80e78eb7d88c3c2361a98daaabb06256e4500db941" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "getrandom" version = "0.2.8" @@ -742,9 +721,9 @@ dependencies = [ [[package]] name = "iana-time-zone" -version = "0.1.51" +version = "0.1.53" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f5a6ef98976b22b3b7f2f3a806f858cb862044cfa66805aa3ad84cb3d3b785ed" +checksum = "64c122667b287044802d6ce17ee2ddf13207ed924c712de9a66a5814d5b64765" dependencies = [ "android_system_properties", "core-foundation-sys", @@ -756,9 +735,9 @@ dependencies = [ [[package]] name = "iana-time-zone-haiku" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fde6edd6cef363e9359ed3c98ba64590ba9eecba2293eb5a723ab32aee8926aa" +checksum = "0703ae284fc167426161c2e3f1da3ea71d94b21bedbcc9494e92b28e334e3dca" dependencies = [ "cxx", "cxx-build", @@ -914,9 +893,9 @@ dependencies = [ [[package]] name = "io-lifetimes" -version = "0.7.3" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ea37f355c05dde75b84bba2d767906ad522e97cd9e2eef2be7a4ab7fb442c06" +checksum = "59ce5ef949d49ee85593fc4d3f3f95ad61657076395cbbce23e2121fc5542074" [[package]] name = "itertools" @@ -971,9 +950,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.135" +version = "0.2.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "68783febc7782c6c5cb401fbda4de5a9898be1762314da0bb2c10ced61f18b0c" +checksum = "fc7fcc620a3bff7cdd7a365be3376c97191aeaccc2a603e600951e452615bf89" [[package]] name = "link-cplusplus" @@ -1111,9 +1090,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.1" +version = "1.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" +checksum = "f6058e64324c71e02bc2b150e4f3bc8286db6c83092132ffa3f6b1eab0f9def5" dependencies = [ "hermit-abi", "libc", @@ -1133,9 +1112,9 @@ checksum = "0ab1bc2a289d34bd04a330323ac98a1b4bc82c9d9fcb1e66b63caa84da26b575" [[package]] name = "os_str_bytes" -version = "6.3.0" +version = "6.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff7415e9ae3fff1225851df9e0d9e4e5479f947619774677a63572e55e80eff" +checksum = "3baf96e39c5359d2eb0dd6ccb42c62b91d9678aa68160d261b9e0ccbf9e9dea9" [[package]] name = "parking_lot" @@ -1259,9 +1238,9 @@ checksum = "7c68cb38ed13fd7bc9dd5db8f165b7c8d9c1a315104083a2b10f11354c2af97f" [[package]] name = "ppv-lite86" -version = "0.2.16" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb9f9e6e233e5c4a35559a617bf40a4ec447db2e84c20b55a6f83167b7e57872" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" [[package]] name = "proc-macro-error" @@ -1289,9 +1268,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.46" +version = "1.0.47" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94e2ef8dbfc347b10c094890f778ee2e36ca9bb4262e86dc99cd217e35f3470b" +checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725" dependencies = [ "unicode-ident", ] @@ -1402,9 +1381,9 @@ dependencies = [ [[package]] name = "regex-syntax" -version = "0.6.27" +version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3f87b73ce11b1619a3c6332f45341e0047173771e8b8b73f87bfeefb7b56244" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" [[package]] name = "regress" @@ -1423,9 +1402,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2" [[package]] name = "rustix" -version = "0.35.11" +version = "0.35.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbb2fda4666def1433b1b05431ab402e42a1084285477222b72d6c564c417cef" +checksum = "727a1a6d65f786ec22df8a81ca3121107f235970dc1705ed681d3e6e8b9cd5f9" dependencies = [ "bitflags", "errno", @@ -1644,9 +1623,9 @@ dependencies = [ [[package]] name = "textwrap" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" [[package]] name = "thiserror" @@ -1893,46 +1872,60 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows-sys" -version = "0.36.1" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ea04155a16a59f9eab786fe12a4a450e75cdb175f9e0d80da1e17db09f55b8d2" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ + "windows_aarch64_gnullvm", "windows_aarch64_msvc", "windows_i686_gnu", "windows_i686_msvc", "windows_x86_64_gnu", + "windows_x86_64_gnullvm", "windows_x86_64_msvc", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" + [[package]] name = "windows_aarch64_msvc" -version = "0.36.1" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9bb8c3fd39ade2d67e9874ac4f3db21f0d710bee00fe7cab16949ec184eeaa47" +checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" [[package]] name = "windows_i686_gnu" -version = "0.36.1" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "180e6ccf01daf4c426b846dfc66db1fc518f074baa793aa7d9b9aaeffad6a3b6" +checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" [[package]] name = "windows_i686_msvc" -version = "0.36.1" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e2e7917148b2812d1eeafaeb22a97e4813dfa60a3f8f78ebe204bcc88f12f024" +checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" [[package]] name = "windows_x86_64_gnu" -version = "0.36.1" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4dcd171b8776c41b97521e5da127a2d86ad280114807d0b2ab1e462bc764d9e1" +checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" [[package]] name = "windows_x86_64_msvc" -version = "0.36.1" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c811ca4a8c853ef420abd8592ba53ddbbac90410fab6903b3e79972a631f7680" +checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" [[package]] name = "writeable" @@ -1975,9 +1968,9 @@ dependencies = [ [[package]] name = "zerofrom-derive" -version = "0.1.0" +version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8785f47d6062c1932866147f91297286a9f350b3070e9d9f0b6078e37d623c1a" +checksum = "2e8aa86add9ddbd2409c1ed01e033cd457d79b1b1229b64922c25095c595e829" dependencies = [ "proc-macro2", "quote", diff --git a/boa_engine/Cargo.toml b/boa_engine/Cargo.toml index 2d6fd774ee2..579e3e83946 100644 --- a/boa_engine/Cargo.toml +++ b/boa_engine/Cargo.toml @@ -37,7 +37,6 @@ boa_profiler.workspace = true boa_macros.workspace = true boa_ast.workspace = true boa_parser.workspace = true -gc = "0.4.1" serde = { version = "1.0.147", features = ["derive", "rc"] } serde_json = "1.0.87" rand = "0.8.5" diff --git a/boa_engine/src/builtins/async_generator/mod.rs b/boa_engine/src/builtins/async_generator/mod.rs index e6ea7180641..768dfd48a62 100644 --- a/boa_engine/src/builtins/async_generator/mod.rs +++ b/boa_engine/src/builtins/async_generator/mod.rs @@ -18,7 +18,7 @@ use crate::{ vm::GeneratorResumeKind, Context, JsError, JsResult, }; -use boa_gc::{Cell, Finalize, Gc, Trace}; +use boa_gc::{Finalize, Gc, GcCell, Trace}; use boa_profiler::Profiler; use std::collections::VecDeque; @@ -56,7 +56,7 @@ pub struct AsyncGenerator { pub(crate) state: AsyncGeneratorState, /// The `[[AsyncGeneratorContext]]` internal slot. - pub(crate) context: Option>>, + pub(crate) context: Option>>, /// The `[[AsyncGeneratorQueue]]` internal slot. pub(crate) queue: VecDeque, @@ -511,7 +511,7 @@ impl AsyncGenerator { pub(crate) fn resume( generator: &JsObject, state: AsyncGeneratorState, - generator_context: &Gc>, + generator_context: &Gc>, completion: (JsResult, bool), context: &mut Context, ) { diff --git a/boa_engine/src/builtins/function/mod.rs b/boa_engine/src/builtins/function/mod.rs index 5cc70deec57..ebd927fd795 100644 --- a/boa_engine/src/builtins/function/mod.rs +++ b/boa_engine/src/builtins/function/mod.rs @@ -34,7 +34,7 @@ use boa_ast::{ operations::{bound_names, contains, lexically_declared_names, ContainsSymbol}, StatementList, }; -use boa_gc::{self, custom_trace, Finalize, Gc, Trace}; +use boa_gc::{self, custom_trace, Finalize, Gc, GcCell, Trace}; use boa_interner::Sym; use boa_parser::Parser; use boa_profiler::Profiler; @@ -178,7 +178,7 @@ unsafe impl Trace for ClassFieldDefinition { /// with `Any::downcast_ref` and `Any::downcast_mut` to recover the original /// type. #[derive(Clone, Debug, Trace, Finalize)] -pub struct Captures(Gc>>); +pub struct Captures(Gc>>); impl Captures { /// Creates a new capture context. @@ -186,7 +186,7 @@ impl Captures { where T: NativeObject, { - Self(Gc::new(boa_gc::Cell::new(Box::new(captures)))) + Self(Gc::new(GcCell::new(Box::new(captures)))) } /// Casts `Captures` to `Any` @@ -194,7 +194,7 @@ impl Captures { /// # Panics /// /// Panics if it's already borrowed as `&mut Any` - pub fn as_any(&self) -> boa_gc::Ref<'_, dyn Any> { + pub fn as_any(&self) -> boa_gc::GcCellRef<'_, dyn Any> { Ref::map(self.0.borrow(), |data| data.deref().as_any()) } @@ -203,7 +203,7 @@ impl Captures { /// # Panics /// /// Panics if it's already borrowed as `&mut Any` - pub fn as_mut_any(&self) -> boa_gc::RefMut<'_, Box, dyn Any> { + pub fn as_mut_any(&self) -> boa_gc::GcCellRefMut<'_, Box, dyn Any> { RefMut::map(self.0.borrow_mut(), |data| data.deref_mut().as_mut_any()) } } diff --git a/boa_engine/src/builtins/generator/mod.rs b/boa_engine/src/builtins/generator/mod.rs index 74eea06fc13..7599dd90742 100644 --- a/boa_engine/src/builtins/generator/mod.rs +++ b/boa_engine/src/builtins/generator/mod.rs @@ -20,7 +20,7 @@ use crate::{ vm::{CallFrame, GeneratorResumeKind, ReturnType}, Context, JsError, JsResult, }; -use boa_gc::{Cell, Finalize, Gc, Trace}; +use boa_gc::{Finalize, Gc, GcCell, Trace}; use boa_profiler::Profiler; /// Indicates the state of a generator. @@ -52,7 +52,7 @@ pub struct Generator { pub(crate) state: GeneratorState, /// The `[[GeneratorContext]]` internal slot. - pub(crate) context: Option>>, + pub(crate) context: Option>>, } impl BuiltIn for Generator { diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index ae4ebef81e8..7bdf64cbc7a 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/boa_engine/src/builtins/promise/mod.rs @@ -21,7 +21,7 @@ use crate::{ value::JsValue, Context, JsError, JsResult, }; -use boa_gc::{Cell as GcCell, Finalize, Gc, Trace}; +use boa_gc::{Finalize, Gc, GcCell, Trace}; use boa_profiler::Profiler; use std::{cell::Cell, rc::Rc}; use tap::{Conv, Pipe}; @@ -118,7 +118,7 @@ impl PromiseCapability { // 2. NOTE: C is assumed to be a constructor function that supports the parameter conventions of the Promise constructor (see 27.2.3.1). // 3. Let promiseCapability be the PromiseCapability Record { [[Promise]]: undefined, [[Resolve]]: undefined, [[Reject]]: undefined }. - let promise_capability = Gc::new(boa_gc::Cell::new(RejectResolve { + let promise_capability = Gc::new(GcCell::new(RejectResolve { reject: JsValue::undefined(), resolve: JsValue::undefined(), })); diff --git a/boa_engine/src/bytecompiler/mod.rs b/boa_engine/src/bytecompiler/mod.rs index 33dec83bbc2..8bff029b4e8 100644 --- a/boa_engine/src/bytecompiler/mod.rs +++ b/boa_engine/src/bytecompiler/mod.rs @@ -30,7 +30,7 @@ use boa_ast::{ }, Declaration, Expression, Statement, StatementList, StatementListItem, }; -use boa_gc::Gc; +use boa_gc::{Gc, GcCell}; use boa_interner::{Interner, Sym}; use rustc_hash::FxHashMap; use std::mem::size_of; @@ -265,7 +265,7 @@ impl<'b> ByteCompiler<'b> { #[inline] fn push_compile_environment( &mut self, - environment: Gc>, + environment: Gc>, ) -> usize { let index = self.code_block.compile_environments.len(); self.code_block.compile_environments.push(environment); diff --git a/boa_engine/src/environments/compile.rs b/boa_engine/src/environments/compile.rs index 37aa9a64ec4..1ff6f3faf78 100644 --- a/boa_engine/src/environments/compile.rs +++ b/boa_engine/src/environments/compile.rs @@ -2,7 +2,7 @@ use crate::{ environments::runtime::BindingLocator, property::PropertyDescriptor, Context, JsString, JsValue, }; use boa_ast::expression::Identifier; -use boa_gc::{Cell, Finalize, Gc, Trace}; +use boa_gc::{Finalize, Gc, GcCell, Trace}; use rustc_hash::FxHashMap; @@ -22,7 +22,7 @@ struct CompileTimeBinding { /// A compile time environment also indicates, if it is a function environment. #[derive(Debug, Finalize, Trace)] pub(crate) struct CompileTimeEnvironment { - outer: Option>>, + outer: Option>>, environment_index: usize, #[unsafe_ignore_trace] bindings: FxHashMap, @@ -223,7 +223,7 @@ impl Context { let environment_index = self.realm.compile_env.borrow().environment_index + 1; let outer = self.realm.compile_env.clone(); - self.realm.compile_env = Gc::new(Cell::new(CompileTimeEnvironment { + self.realm.compile_env = Gc::new(GcCell::new(CompileTimeEnvironment { outer: Some(outer), environment_index, bindings: FxHashMap::default(), @@ -241,7 +241,7 @@ impl Context { #[inline] pub(crate) fn pop_compile_time_environment( &mut self, - ) -> (usize, Gc>) { + ) -> (usize, Gc>) { let current_env_borrow = self.realm.compile_env.borrow(); if let Some(outer) = ¤t_env_borrow.outer { let outer_clone = outer.clone(); diff --git a/boa_engine/src/environments/runtime.rs b/boa_engine/src/environments/runtime.rs index 3314eb75e51..c06ab965724 100644 --- a/boa_engine/src/environments/runtime.rs +++ b/boa_engine/src/environments/runtime.rs @@ -3,7 +3,7 @@ use std::cell::Cell; use crate::{ environments::CompileTimeEnvironment, error::JsNativeError, object::JsObject, Context, JsValue, }; -use boa_gc::{Cell as GcCell, Finalize, Gc, Trace}; +use boa_gc::{Finalize, Gc, GcCell, Trace}; use boa_ast::expression::Identifier; use rustc_hash::FxHashSet; diff --git a/boa_engine/src/job.rs b/boa_engine/src/job.rs index 2a0988b2e57..f24be132a3a 100644 --- a/boa_engine/src/job.rs +++ b/boa_engine/src/job.rs @@ -1,5 +1,5 @@ use crate::{prelude::JsObject, Context, JsResult, JsValue}; -use gc::{Finalize, Trace}; +use boa_gc::{Finalize, Trace}; /// `JobCallback` records /// diff --git a/boa_engine/src/object/jsobject.rs b/boa_engine/src/object/jsobject.rs index d1afe8f99aa..6bcf077f9bd 100644 --- a/boa_engine/src/object/jsobject.rs +++ b/boa_engine/src/object/jsobject.rs @@ -10,7 +10,7 @@ use crate::{ value::PreferredType, Context, JsResult, JsValue, }; -use boa_gc::{self, Finalize, Gc, Trace}; +use boa_gc::{self, Finalize, Gc, GcCell, Trace}; use rustc_hash::FxHashMap; use std::{ cell::RefCell, @@ -21,15 +21,15 @@ use std::{ }; /// A wrapper type for an immutably borrowed type T. -pub type Ref<'a, T> = boa_gc::Ref<'a, T>; +pub type Ref<'a, T> = boa_gc::GcCellRef<'a, T>; /// A wrapper type for a mutably borrowed type T. -pub type RefMut<'a, T, U> = boa_gc::RefMut<'a, T, U>; +pub type RefMut<'a, T, U> = boa_gc::GcCellRefMut<'a, T, U>; /// Garbage collected `Object`. #[derive(Trace, Finalize, Clone, Default)] pub struct JsObject { - inner: Gc>, + inner: Gc>, } impl JsObject { @@ -37,7 +37,7 @@ impl JsObject { #[inline] fn from_object(object: Object) -> Self { Self { - inner: Gc::new(boa_gc::Cell::new(object)), + inner: Gc::new(GcCell::new(object)), } } @@ -738,9 +738,9 @@ Cannot both specify accessors and a value or writable attribute", } } -impl AsRef> for JsObject { +impl AsRef> for JsObject { #[inline] - fn as_ref(&self) -> &boa_gc::Cell { + fn as_ref(&self) -> &GcCell { &self.inner } } diff --git a/boa_engine/src/realm.rs b/boa_engine/src/realm.rs index 588f01fd16c..ab8cf8dfbd9 100644 --- a/boa_engine/src/realm.rs +++ b/boa_engine/src/realm.rs @@ -8,7 +8,7 @@ use crate::{ environments::{CompileTimeEnvironment, DeclarativeEnvironmentStack}, object::{GlobalPropertyMap, JsObject, JsPrototype, ObjectData, PropertyMap}, }; -use boa_gc::{Cell, Gc}; +use boa_gc::{Gc, GcCell}; use boa_profiler::Profiler; /// Representation of a Realm. @@ -21,7 +21,7 @@ pub struct Realm { pub(crate) global_property_map: PropertyMap, pub(crate) global_prototype: JsPrototype, pub(crate) environments: DeclarativeEnvironmentStack, - pub(crate) compile_env: Gc>, + pub(crate) compile_env: Gc>, } impl Realm { @@ -33,7 +33,7 @@ impl Realm { // Allow identification of the global object easily let global_object = JsObject::from_proto_and_data(None, ObjectData::global()); - let global_compile_environment = Gc::new(Cell::new(CompileTimeEnvironment::new_global())); + let global_compile_environment = Gc::new(GcCell::new(CompileTimeEnvironment::new_global())); Self { global_object, diff --git a/boa_engine/src/string/mod.rs b/boa_engine/src/string/mod.rs index 2b6ea514eb8..2932e76b9b7 100644 --- a/boa_engine/src/string/mod.rs +++ b/boa_engine/src/string/mod.rs @@ -24,7 +24,7 @@ mod common; use crate::{builtins::string::is_trimmable_whitespace, JsBigInt}; -use boa_gc::{unsafe_empty_trace, Finalize, Trace}; +use boa_gc::{empty_trace, Finalize, Trace}; pub use boa_macros::utf16; use std::{ @@ -292,7 +292,7 @@ sa::assert_eq_size!(JsString, *const ()); // Safety: `JsString` does not contain any objects which needs to be traced, so this is safe. unsafe impl Trace for JsString { - unsafe_empty_trace!(); + empty_trace!(); } impl JsString { diff --git a/boa_engine/src/symbol.rs b/boa_engine/src/symbol.rs index f7f44488294..c0b69c841f2 100644 --- a/boa_engine/src/symbol.rs +++ b/boa_engine/src/symbol.rs @@ -16,7 +16,7 @@ //! [mdn]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Symbol use crate::{js_string, string::utf16, JsString}; -use boa_gc::{unsafe_empty_trace, Finalize, Trace}; +use boa_gc::{empty_trace, Finalize, Trace}; use std::{ cell::Cell, hash::{Hash, Hasher}, @@ -255,7 +255,7 @@ pub struct JsSymbol { // Safety: JsSymbol does not contain any objects which needs to be traced, // so this is safe. unsafe impl Trace for JsSymbol { - unsafe_empty_trace!(); + empty_trace!(); } impl JsSymbol { diff --git a/boa_engine/src/vm/code_block.rs b/boa_engine/src/vm/code_block.rs index a8444e20725..02f52e097f2 100644 --- a/boa_engine/src/vm/code_block.rs +++ b/boa_engine/src/vm/code_block.rs @@ -24,7 +24,7 @@ use crate::{ Context, JsResult, JsString, JsValue, }; use boa_ast::{expression::Identifier, function::FormalParameterList}; -use boa_gc::{Cell, Finalize, Gc, Trace}; +use boa_gc::{Finalize, Gc, GcCell, Trace}; use boa_interner::{Interner, Sym, ToInternedString}; use boa_profiler::Profiler; use std::{collections::VecDeque, convert::TryInto, mem::size_of}; @@ -103,7 +103,7 @@ pub struct CodeBlock { pub(crate) arguments_binding: Option, /// Compile time environments in this function. - pub(crate) compile_environments: Vec>>, + pub(crate) compile_environments: Vec>>, /// The `[[IsClassConstructor]]` internal slot. pub(crate) is_class_constructor: bool, @@ -1099,7 +1099,7 @@ impl JsObject { prototype, ObjectData::generator(Generator { state: GeneratorState::SuspendedStart, - context: Some(Gc::new(Cell::new(GeneratorContext { + context: Some(Gc::new(GcCell::new(GeneratorContext { environments, call_frame, stack, @@ -1242,7 +1242,7 @@ impl JsObject { prototype, ObjectData::async_generator(AsyncGenerator { state: AsyncGeneratorState::SuspendedStart, - context: Some(Gc::new(Cell::new(GeneratorContext { + context: Some(Gc::new(GcCell::new(GeneratorContext { environments, call_frame, stack, diff --git a/boa_examples/Cargo.toml b/boa_examples/Cargo.toml index 28b1be49752..b6135a7dc82 100644 --- a/boa_examples/Cargo.toml +++ b/boa_examples/Cargo.toml @@ -17,4 +17,3 @@ boa_ast.workspace = true boa_interner.workspace = true boa_gc.workspace = true boa_parser.workspace = true -gc = "0.4.1" diff --git a/boa_gc/Cargo.toml b/boa_gc/Cargo.toml index 5cca97b53be..ceaa195cdc6 100644 --- a/boa_gc/Cargo.toml +++ b/boa_gc/Cargo.toml @@ -11,7 +11,8 @@ repository.workspace = true rust-version.workspace = true [dependencies] -gc = { version = "0.4.1", features = ["derive"] } +boa_profiler.workspace = true +boa_macros.workspace = true # Optional Dependencies measureme = { version = "10.1.0", optional = true } diff --git a/boa_gc/src/cell.rs b/boa_gc/src/cell.rs new file mode 100644 index 00000000000..36607629347 --- /dev/null +++ b/boa_gc/src/cell.rs @@ -0,0 +1,594 @@ +//! A garbage collected cell implementation +use std::cell::{Cell, UnsafeCell}; +use std::cmp::Ordering; +use std::fmt::{self, Debug, Display}; +use std::hash::Hash; +use std::ops::{Deref, DerefMut}; + +use crate::trace::{Finalize, Trace}; + +/// `BorrowFlag` represent the internal state of a `GcCell` and +/// keeps track of the amount of current borrows. +#[derive(Copy, Clone)] +pub(crate) struct BorrowFlag(usize); + +/// `BorrowState` represents the various states of a `BorrowFlag` +/// +/// - Reading: the value is currently being read/borrowed. +/// - Writing: the value is currently being written/borrowed mutably. +/// - Unused: the value is currently unrooted. +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub(crate) enum BorrowState { + Reading, + Writing, + Unused, +} + +const ROOT: usize = 1; +const WRITING: usize = !1; +const UNUSED: usize = 0; + +/// The base borrowflag init is rooted, and has no outstanding borrows. +pub(crate) const BORROWFLAG_INIT: BorrowFlag = BorrowFlag(ROOT); + +impl BorrowFlag { + /// Check the current `BorrowState` of `BorrowFlag`. + #[inline] + pub(crate) fn borrowed(self) -> BorrowState { + match self.0 & !ROOT { + UNUSED => BorrowState::Unused, + WRITING => BorrowState::Writing, + _ => BorrowState::Reading, + } + } + + /// Check whether the borrow bit is flagged. + #[inline] + pub(crate) fn rooted(self) -> bool { + self.0 & ROOT > 0 + } + + /// Set the `BorrowFlag`'s state to writing. + #[inline] + pub(crate) fn set_writing(self) -> Self { + // Set every bit other than the root bit, which is preserved + Self(self.0 | WRITING) + } + + /// Remove the root flag on `BorrowFlag` + #[inline] + pub(crate) fn set_unused(self) -> Self { + // Clear every bit other than the root bit, which is preserved + Self(self.0 & ROOT) + } + + /// Increments the counter for a new borrow. + /// + /// # Panic + /// - This method will panic if the current `BorrowState` is writing. + /// - This method will panic after incrementing if the borrow count overflows. + #[inline] + pub(crate) fn add_reading(self) -> Self { + assert!(self.borrowed() != BorrowState::Writing); + // Add 1 to the integer starting at the second binary digit. As our + // borrowstate is not writing, we know that overflow cannot happen, so + // this is equivalent to the following, more complicated, expression: + // + // BorrowFlag((self.0 & ROOT) | (((self.0 >> 1) + 1) << 1)) + let flags = Self(self.0 + 0b10); + + // This will fail if the borrow count overflows, which shouldn't happen, + // but let's be safe + { + assert!(flags.borrowed() == BorrowState::Reading); + } + flags + } + + /// Decrements the counter to remove a borrow. + /// + /// # Panic + /// - This method will panic if the current `BorrowState` is not reading. + #[inline] + pub(crate) fn sub_reading(self) -> Self { + assert!(self.borrowed() == BorrowState::Reading); + // Subtract 1 from the integer starting at the second binary digit. As + // our borrowstate is not writing or unused, we know that overflow or + // undeflow cannot happen, so this is equivalent to the following, more + // complicated, expression: + // + // BorrowFlag((self.0 & ROOT) | (((self.0 >> 1) - 1) << 1)) + Self(self.0 - 0b10) + } + + /// Set the root flag on the `BorrowFlag`. + #[inline] + pub(crate) fn set_rooted(self, rooted: bool) -> Self { + // Preserve the non-root bits + Self((self.0 & !ROOT) | (usize::from(rooted))) + } +} + +impl Debug for BorrowFlag { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("BorrowFlag") + .field("Rooted", &self.rooted()) + .field("State", &self.borrowed()) + .finish() + } +} + +/// A mutable memory location with dynamically checked borrow rules +/// that can be used inside of a garbage-collected pointer. +/// +/// This object is a `RefCell` that can be used inside of a `Gc`. +pub struct GcCell { + pub(crate) flags: Cell, + pub(crate) cell: UnsafeCell, +} + +impl GcCell { + /// Creates a new `GcCell` containing `value`. + #[inline] + pub fn new(value: T) -> Self { + Self { + flags: Cell::new(BORROWFLAG_INIT), + cell: UnsafeCell::new(value), + } + } + + /// Consumes the `GcCell`, returning the wrapped value. + #[inline] + pub fn into_inner(self) -> T { + self.cell.into_inner() + } +} + +impl GcCell { + /// Immutably borrows the wrapped value. + /// + /// The borrow lasts until the returned `GcCellRef` exits scope. + /// Multiple immutable borrows can be taken out at the same time. + /// + /// # Panics + /// + /// Panics if the value is currently mutably borrowed. + #[inline] + pub fn borrow(&self) -> GcCellRef<'_, T> { + match self.try_borrow() { + Ok(value) => value, + Err(e) => panic!("{}", e), + } + } + + /// Mutably borrows the wrapped value. + /// + /// The borrow lasts until the returned `GcCellRefMut` exits scope. + /// The value cannot be borrowed while this borrow is active. + /// + /// # Panics + /// + /// Panics if the value is currently borrowed. + #[inline] + pub fn borrow_mut(&self) -> GcCellRefMut<'_, T> { + match self.try_borrow_mut() { + Ok(value) => value, + Err(e) => panic!("{}", e), + } + } + + /// Immutably borrows the wrapped value, returning an error if the value is currently mutably + /// borrowed. + /// + /// The borrow lasts until the returned `GcCellRef` exits scope. Multiple immutable borrows can be + /// taken out at the same time. + /// + /// This is the non-panicking variant of [`borrow`](#method.borrow). + /// + /// # Errors + /// + /// Returns an `Err` if the value is currently mutably borrowed. + pub fn try_borrow(&self) -> Result, BorrowError> { + if self.flags.get().borrowed() == BorrowState::Writing { + return Err(BorrowError); + } + self.flags.set(self.flags.get().add_reading()); + + // SAFETY: calling value on a rooted value may cause Undefined Behavior + unsafe { + Ok(GcCellRef { + flags: &self.flags, + value: &*self.cell.get(), + }) + } + } + + /// Mutably borrows the wrapped value, returning an error if the value is currently borrowed. + /// + /// The borrow lasts until the returned `GcCellRefMut` exits scope. + /// The value cannot be borrowed while this borrow is active. + /// + /// This is the non-panicking variant of [`borrow_mut`](#method.borrow_mut). + /// + /// # Errors + /// + /// Returns an `Err` if the value is currently borrowed. + pub fn try_borrow_mut(&self) -> Result, BorrowMutError> { + if self.flags.get().borrowed() != BorrowState::Unused { + return Err(BorrowMutError); + } + self.flags.set(self.flags.get().set_writing()); + + // SAFETY: This is safe as the value is rooted if it was not previously rooted, + // so it cannot be dropped. + unsafe { + // Force the val_ref's contents to be rooted for the duration of the + // mutable borrow + if !self.flags.get().rooted() { + (*self.cell.get()).root(); + } + + Ok(GcCellRefMut { + gc_cell: self, + value: &mut *self.cell.get(), + }) + } + } +} + +/// An error returned by [`GcCell::try_borrow`](struct.GcCell.html#method.try_borrow). +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Default, Hash)] +pub struct BorrowError; + +impl Display for BorrowError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt("GcCell already mutably borrowed", f) + } +} + +/// An error returned by [`GcCell::try_borrow_mut`](struct.GcCell.html#method.try_borrow_mut). +#[derive(Debug, Clone, Copy, Eq, PartialEq, Ord, PartialOrd, Default, Hash)] +pub struct BorrowMutError; + +impl Display for BorrowMutError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + Display::fmt("GcCell already borrowed", f) + } +} + +impl Finalize for GcCell {} + +// SAFETY: GcCell maintains it's own BorrowState and rootedness. GcCell's implementation +// focuses on only continuing Trace based methods while the cell state is not written. +// Implementing a Trace while the cell is being written to or incorrectly implementing Trace +// on GcCell's value may cause Undefined Behavior +unsafe impl Trace for GcCell { + #[inline] + unsafe fn trace(&self) { + match self.flags.get().borrowed() { + BorrowState::Writing => (), + // SAFETY: Please see GcCell's Trace impl Safety note. + _ => unsafe { (*self.cell.get()).trace() }, + } + } + + #[inline] + unsafe fn weak_trace(&self) { + match self.flags.get().borrowed() { + BorrowState::Writing => (), + // SAFETY: Please see GcCell's Trace impl Safety note. + _ => unsafe { (*self.cell.get()).weak_trace() }, + } + } + + unsafe fn root(&self) { + assert!(!self.flags.get().rooted(), "Can't root a GcCell twice!"); + self.flags.set(self.flags.get().set_rooted(true)); + + match self.flags.get().borrowed() { + BorrowState::Writing => (), + // SAFETY: Please see GcCell's Trace impl Safety note. + _ => unsafe { (*self.cell.get()).root() }, + } + } + + #[inline] + unsafe fn unroot(&self) { + assert!(self.flags.get().rooted(), "Can't unroot a GcCell twice!"); + self.flags.set(self.flags.get().set_rooted(false)); + + match self.flags.get().borrowed() { + BorrowState::Writing => (), + // SAFETY: Please see GcCell's Trace impl Safety note. + _ => unsafe { (*self.cell.get()).unroot() }, + } + } + + #[inline] + fn run_finalizer(&self) { + Finalize::finalize(self); + match self.flags.get().borrowed() { + BorrowState::Writing => (), + // SAFETY: Please see GcCell's Trace impl Safety note. + _ => unsafe { (*self.cell.get()).run_finalizer() }, + } + } +} + +/// A wrapper type for an immutably borrowed value from a `GcCell`. +pub struct GcCellRef<'a, T: ?Sized + 'static> { + pub(crate) flags: &'a Cell, + pub(crate) value: &'a T, +} + +impl<'a, T: ?Sized> GcCellRef<'a, T> { + /// Copies a `GcCellRef`. + /// + /// The `GcCell` is already immutably borrowed, so this cannot fail. + /// + /// This is an associated function that needs to be used as + /// `GcCellRef::clone(...)`. A `Clone` implementation or a method + /// would interfere with the use of `c.borrow().clone()` to clone + /// the contents of a `GcCell`. + #[inline] + #[allow(clippy::should_implement_trait)] + #[must_use] + pub fn clone(orig: &GcCellRef<'a, T>) -> GcCellRef<'a, T> { + orig.flags.set(orig.flags.get().add_reading()); + GcCellRef { + flags: orig.flags, + value: orig.value, + } + } + + /// Makes a new `GcCellRef` from a component of the borrowed data. + /// + /// The `GcCell` is already immutably borrowed, so this cannot fail. + /// + /// This is an associated function that needs to be used as `GcCellRef::map(...)`. + /// A method would interfere with methods of the same name on the contents + /// of a `GcCellRef` used through `Deref`. + #[inline] + pub fn map(orig: Self, f: F) -> GcCellRef<'a, U> + where + U: ?Sized, + F: FnOnce(&T) -> &U, + { + let ret = GcCellRef { + flags: orig.flags, + value: f(orig.value), + }; + + // We have to tell the compiler not to call the destructor of GcCellRef, + // because it will update the borrow flags. + std::mem::forget(orig); + + ret + } + + /// Splits a `GcCellRef` into multiple `GcCellRef`s for different components of the borrowed data. + /// + /// The `GcCell` is already immutably borrowed, so this cannot fail. + /// + /// This is an associated function that needs to be used as `GcCellRef::map_split(...)`. + /// A method would interfere with methods of the same name on the contents of a `GcCellRef` used through `Deref`. + #[inline] + pub fn map_split(orig: Self, f: F) -> (GcCellRef<'a, U>, GcCellRef<'a, V>) + where + U: ?Sized, + V: ?Sized, + F: FnOnce(&T) -> (&U, &V), + { + let (a, b) = f(orig.value); + + orig.flags.set(orig.flags.get().add_reading()); + + let ret = ( + GcCellRef { + flags: orig.flags, + value: a, + }, + GcCellRef { + flags: orig.flags, + value: b, + }, + ); + + // We have to tell the compiler not to call the destructor of GcCellRef, + // because it will update the borrow flags. + std::mem::forget(orig); + + ret + } +} + +impl<'a, T: ?Sized> Deref for GcCellRef<'a, T> { + type Target = T; + + #[inline] + fn deref(&self) -> &T { + self.value + } +} + +impl<'a, T: ?Sized> Drop for GcCellRef<'a, T> { + fn drop(&mut self) { + debug_assert!(self.flags.get().borrowed() == BorrowState::Reading); + self.flags.set(self.flags.get().sub_reading()); + } +} + +impl<'a, T: ?Sized + Debug> Debug for GcCellRef<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Debug::fmt(&**self, f) + } +} + +impl<'a, T: ?Sized + Display> Display for GcCellRef<'a, T> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&**self, f) + } +} + +/// A wrapper type for a mutably borrowed value from a `GcCell`. +pub struct GcCellRefMut<'a, T: Trace + ?Sized + 'static, U: ?Sized = T> { + pub(crate) gc_cell: &'a GcCell, + pub(crate) value: &'a mut U, +} + +impl<'a, T: Trace + ?Sized, U: ?Sized> GcCellRefMut<'a, T, U> { + /// Makes a new `GcCellRefMut` for a component of the borrowed data, e.g., an enum + /// variant. + /// + /// The `GcCellRefMut` is already mutably borrowed, so this cannot fail. + /// + /// This is an associated function that needs to be used as + /// `GcCellRefMut::map(...)`. A method would interfere with methods of the same + /// name on the contents of a `GcCell` used through `Deref`. + #[inline] + pub fn map(orig: Self, f: F) -> GcCellRefMut<'a, T, V> + where + V: ?Sized, + F: FnOnce(&mut U) -> &mut V, + { + // SAFETY: This is safe as `GcCellRefMut` is already borrowed, so the value is rooted. + let value = unsafe { &mut *(orig.value as *mut U) }; + + let ret = GcCellRefMut { + gc_cell: orig.gc_cell, + value: f(value), + }; + + // We have to tell the compiler not to call the destructor of GcCellRefMut, + // because it will update the borrow flags. + std::mem::forget(orig); + + ret + } +} + +impl<'a, T: Trace + ?Sized, U: ?Sized> Deref for GcCellRefMut<'a, T, U> { + type Target = U; + + #[inline] + fn deref(&self) -> &U { + self.value + } +} + +impl<'a, T: Trace + ?Sized, U: ?Sized> DerefMut for GcCellRefMut<'a, T, U> { + #[inline] + fn deref_mut(&mut self) -> &mut U { + self.value + } +} + +impl<'a, T: Trace + ?Sized, U: ?Sized> Drop for GcCellRefMut<'a, T, U> { + #[inline] + fn drop(&mut self) { + debug_assert!(self.gc_cell.flags.get().borrowed() == BorrowState::Writing); + // Restore the rooted state of the GcCell's contents to the state of the GcCell. + // During the lifetime of the GcCellRefMut, the GcCell's contents are rooted. + if !self.gc_cell.flags.get().rooted() { + // SAFETY: If `GcCell` is no longer rooted, then unroot it. This should be safe + // as the internal `GcBox` should be guaranteed to have at least 1 root. + unsafe { + (*self.gc_cell.cell.get()).unroot(); + } + } + self.gc_cell + .flags + .set(self.gc_cell.flags.get().set_unused()); + } +} + +impl<'a, T: Trace + ?Sized, U: Debug + ?Sized> Debug for GcCellRefMut<'a, T, U> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Debug::fmt(&**self, f) + } +} + +impl<'a, T: Trace + ?Sized, U: Display + ?Sized> Display for GcCellRefMut<'a, T, U> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&**self, f) + } +} + +// SAFETY: GcCell tracks it's `BorrowState` is `Writing` +unsafe impl Send for GcCell {} + +impl Clone for GcCell { + #[inline] + fn clone(&self) -> Self { + Self::new(self.borrow().clone()) + } +} + +impl Default for GcCell { + #[inline] + fn default() -> Self { + Self::new(Default::default()) + } +} + +#[allow(clippy::inline_always)] +impl PartialEq for GcCell { + #[inline(always)] + fn eq(&self, other: &Self) -> bool { + *self.borrow() == *other.borrow() + } +} + +impl Eq for GcCell {} + +#[allow(clippy::inline_always)] +impl PartialOrd for GcCell { + #[inline(always)] + fn partial_cmp(&self, other: &Self) -> Option { + (*self.borrow()).partial_cmp(&*other.borrow()) + } + + #[inline(always)] + fn lt(&self, other: &Self) -> bool { + *self.borrow() < *other.borrow() + } + + #[inline(always)] + fn le(&self, other: &Self) -> bool { + *self.borrow() <= *other.borrow() + } + + #[inline(always)] + fn gt(&self, other: &Self) -> bool { + *self.borrow() > *other.borrow() + } + + #[inline(always)] + fn ge(&self, other: &Self) -> bool { + *self.borrow() >= *other.borrow() + } +} + +impl Ord for GcCell { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + (*self.borrow()).cmp(&*other.borrow()) + } +} + +impl Debug for GcCell { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.flags.get().borrowed() { + BorrowState::Unused | BorrowState::Reading => f + .debug_struct("GcCell") + .field("flags", &self.flags.get()) + .field("value", &self.borrow()) + .finish(), + BorrowState::Writing => f + .debug_struct("GcCell") + .field("flags", &self.flags.get()) + .field("value", &"") + .finish(), + } + } +} diff --git a/boa_gc/src/internals/ephemeron_box.rs b/boa_gc/src/internals/ephemeron_box.rs new file mode 100644 index 00000000000..420b7fb36c6 --- /dev/null +++ b/boa_gc/src/internals/ephemeron_box.rs @@ -0,0 +1,146 @@ +use crate::trace::Trace; +use crate::{finalizer_safe, GcBox}; +use crate::{Finalize, Gc}; +use std::cell::Cell; +use std::ptr::NonNull; + +/// The inner allocation of an [`Ephemeron`][crate::Ephemeron] pointer. +pub(crate) struct EphemeronBox { + key: Cell>>>, + value: V, +} + +impl EphemeronBox { + pub(crate) fn new(key: &Gc, value: V) -> Self { + Self { + key: Cell::new(Some(key.inner_ptr())), + value, + } + } +} + +impl EphemeronBox { + /// Checks if the key pointer is marked by Trace + #[inline] + pub(crate) fn is_marked(&self) -> bool { + if let Some(key) = self.inner_key() { + key.is_marked() + } else { + false + } + } + + /// Returns some pointer to the `key`'s `GcBox` or None + /// # Panics + /// This method will panic if called while the garbage collector is dropping. + #[inline] + pub(crate) fn inner_key_ptr(&self) -> Option<*mut GcBox> { + assert!(finalizer_safe()); + self.key.get().map(NonNull::as_ptr) + } + + /// Returns some reference to `key`'s `GcBox` or None + #[inline] + pub(crate) fn inner_key(&self) -> Option<&GcBox> { + // SAFETY: This is safe as `EphemeronBox::inner_key_ptr()` will + // fetch either a live `GcBox` or None. The value of `key` is set + // to None in the case where `EphemeronBox` and `key`'s `GcBox` + // entered into `Collector::sweep()` as unmarked. + unsafe { + if let Some(inner_key) = self.inner_key_ptr() { + Some(&*inner_key) + } else { + None + } + } + } + + /// Returns a reference to the value of `key`'s `GcBox` + #[inline] + pub(crate) fn key(&self) -> Option<&K> { + if let Some(key_box) = self.inner_key() { + Some(key_box.value()) + } else { + None + } + } + + /// Returns a reference to `value` + #[inline] + pub(crate) fn value(&self) -> &V { + &self.value + } + + /// Calls [`Trace::weak_trace()`][crate::Trace] on key + #[inline] + fn weak_trace_key(&self) { + if let Some(key) = self.inner_key() { + key.weak_trace_inner(); + } + } + + /// Calls [`Trace::weak_trace()`][crate::Trace] on value + #[inline] + fn weak_trace_value(&self) { + // SAFETY: Value is a sized element that must implement trace. The + // operation is safe as EphemeronBox owns value and `Trace::weak_trace` + // must be implemented on it + unsafe { + self.value().weak_trace(); + } + } +} + +// `EphemeronBox`'s Finalize is special in that if it is determined to be unreachable +// and therefore so has the `GcBox` that `key`stores the pointer to, then we set `key` +// to None to guarantee that we do not access freed memory. +impl Finalize for EphemeronBox { + #[inline] + fn finalize(&self) { + self.key.set(None); + } +} + +// SAFETY: EphemeronBox implements primarly two methods of trace `Trace::is_marked_ephemeron` +// to determine whether the key field is stored and `Trace::weak_trace` which continues the `Trace::weak_trace()` +// into `key` and `value`. +unsafe impl Trace for EphemeronBox { + #[inline] + unsafe fn trace(&self) { + /* An ephemeron is never traced with Phase One Trace */ + } + + /// Checks if the `key`'s `GcBox` has been marked by `Trace::trace()` or `Trace::weak_trace`. + #[inline] + fn is_marked_ephemeron(&self) -> bool { + self.is_marked() + } + + /// Checks if this `EphemeronBox` has already been determined reachable. If so, continue to trace + /// value in `key` and `value`. + #[inline] + unsafe fn weak_trace(&self) { + if self.is_marked() { + self.weak_trace_key(); + self.weak_trace_value(); + } + } + + // EphemeronBox does not implement root. + #[inline] + unsafe fn root(&self) {} + + // EphemeronBox does not implement unroot + #[inline] + unsafe fn unroot(&self) {} + + // An `EphemeronBox`'s key is set to None once it has been finalized. + // + // NOTE: while it is possible for the `key`'s pointer value to be + // resurrected, we should still consider the finalize the ephemeron + // box and set the `key` to None. + #[inline] + fn run_finalizer(&self) { + Finalize::finalize(self); + } +} diff --git a/boa_gc/src/internals/gc_box.rs b/boa_gc/src/internals/gc_box.rs new file mode 100644 index 00000000000..eaca4b48f79 --- /dev/null +++ b/boa_gc/src/internals/gc_box.rs @@ -0,0 +1,195 @@ +use crate::Trace; +use std::cell::Cell; +use std::fmt; +use std::ptr::{self, NonNull}; + +// Age and Weak Flags +const MARK_MASK: usize = 1 << (usize::BITS - 2); +const WEAK_MASK: usize = 1 << (usize::BITS - 1); +const ROOTS_MASK: usize = !(MARK_MASK | WEAK_MASK); +const ROOTS_MAX: usize = ROOTS_MASK; + +/// The `GcBoxheader` contains the `GcBox`'s current state for the `Collector`'s +/// Mark/Sweep as well as a pointer to the next node in the heap. +/// +/// These flags include: +/// - Root Count +/// - Mark Flag Bit +/// - Weak Flag Bit +/// +/// The next node is set by the `Allocator` during initialization and by the +/// `Collector` during the sweep phase. +pub(crate) struct GcBoxHeader { + roots: Cell, + pub(crate) next: Cell>>>, +} + +impl GcBoxHeader { + /// Creates a new `GcBoxHeader` with a root of 1 and next set to None. + #[inline] + pub(crate) fn new() -> Self { + Self { + roots: Cell::new(1), + next: Cell::new(None), + } + } + + /// Creates a new `GcBoxHeader` with the Weak bit at 1 and roots of 1. + #[inline] + pub(crate) fn new_weak() -> Self { + // Set weak_flag + Self { + roots: Cell::new(WEAK_MASK | 1), + next: Cell::new(None), + } + } + + /// Returns the `GcBoxHeader`'s current root count + #[inline] + pub(crate) fn roots(&self) -> usize { + self.roots.get() & ROOTS_MASK + } + + /// Increments `GcBoxHeader`'s root count. + #[inline] + pub(crate) fn inc_roots(&self) { + let roots = self.roots.get(); + + if (roots & ROOTS_MASK) < ROOTS_MAX { + self.roots.set(roots + 1); + } else { + // TODO: implement a better way to handle root overload. + panic!("roots counter overflow"); + } + } + + /// Decreases `GcBoxHeader`'s current root count. + #[inline] + pub(crate) fn dec_roots(&self) { + // Underflow check as a stop gap for current issue when dropping. + if self.roots.get() > 0 { + self.roots.set(self.roots.get() - 1); + } + } + + /// Returns a bool for whether `GcBoxHeader`'s mark bit is 1. + #[inline] + pub(crate) fn is_marked(&self) -> bool { + self.roots.get() & MARK_MASK != 0 + } + + /// Sets `GcBoxHeader`'s mark bit to 1. + #[inline] + pub(crate) fn mark(&self) { + self.roots.set(self.roots.get() | MARK_MASK); + } + + /// Sets `GcBoxHeader`'s mark bit to 0. + #[inline] + pub(crate) fn unmark(&self) { + self.roots.set(self.roots.get() & !MARK_MASK); + } + + /// Returns a bool for whether the `GcBoxHeader`'s weak bit is 1. + #[inline] + pub(crate) fn is_ephemeron(&self) -> bool { + self.roots.get() & WEAK_MASK != 0 + } +} + +impl fmt::Debug for GcBoxHeader { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("GcBoxHeader") + .field("Roots", &self.roots()) + .field("Weak", &self.is_ephemeron()) + .field("Marked", &self.is_marked()) + .finish() + } +} + +/// A garbage collected allocation. +#[derive(Debug)] +pub(crate) struct GcBox { + pub(crate) header: GcBoxHeader, + pub(crate) value: T, +} + +impl GcBox { + /// Returns a new `GcBox` with a rooted `GcBoxHeader`. + pub(crate) fn new(value: T) -> Self { + Self { + header: GcBoxHeader::new(), + value, + } + } + + /// Returns a new `GcBox` with a rooted and weak `GcBoxHeader`. + pub(crate) fn new_weak(value: T) -> Self { + Self { + header: GcBoxHeader::new_weak(), + value, + } + } +} + +impl GcBox { + /// Returns `true` if the two references refer to the same `GcBox`. + pub(crate) fn ptr_eq(this: &Self, other: &Self) -> bool { + // Use .header to ignore fat pointer vtables, to work around + // https://github.com/rust-lang/rust/issues/46139 + ptr::eq(&this.header, &other.header) + } + + /// Marks this `GcBox` and marks through its data. + #[inline] + pub(crate) unsafe fn trace_inner(&self) { + if !self.header.is_marked() && !self.header.is_ephemeron() { + self.header.mark(); + // SAFETY: if `GcBox::trace_inner()` has been called, then, + // this box must have been deemed as reachable via tracing + // from a root, which by extension means that value has not + // been dropped either. + unsafe { + self.value.trace(); + } + } + } + + /// Trace inner data + #[inline] + pub(crate) fn weak_trace_inner(&self) { + // SAFETY: if a `GcBox` has `weak_trace_inner` called, then the inner. + // value must have been deemed as reachable. + unsafe { + self.value.weak_trace(); + } + } + + /// Increases the root count on this `GcBox`. + /// + /// Roots prevent the `GcBox` from being destroyed by the garbage collector. + #[inline] + pub(crate) fn root_inner(&self) { + self.header.inc_roots(); + } + + /// Decreases the root count on this `GcBox`. + /// + /// Roots prevent the `GcBox` from being destroyed by the garbage collector. + #[inline] + pub(crate) fn unroot_inner(&self) { + self.header.dec_roots(); + } + + /// Returns a reference to the `GcBox`'s value. + #[inline] + pub(crate) fn value(&self) -> &T { + &self.value + } + + /// Returns a bool for whether the header is marked. + #[inline] + pub(crate) fn is_marked(&self) -> bool { + self.header.is_marked() + } +} diff --git a/boa_gc/src/internals/mod.rs b/boa_gc/src/internals/mod.rs new file mode 100644 index 00000000000..005c000ade5 --- /dev/null +++ b/boa_gc/src/internals/mod.rs @@ -0,0 +1,5 @@ +mod ephemeron_box; +pub(crate) use ephemeron_box::EphemeronBox; + +mod gc_box; +pub(crate) use gc_box::GcBox; diff --git a/boa_gc/src/lib.rs b/boa_gc/src/lib.rs index ac24531793c..4b26855ea85 100644 --- a/boa_gc/src/lib.rs +++ b/boa_gc/src/lib.rs @@ -1,6 +1,383 @@ //! Garbage collector for the Boa JavaScript engine. -pub use gc::{ - custom_trace, finalizer_safe, force_collect, unsafe_empty_trace, Finalize, Gc, GcCell as Cell, - GcCellRef as Ref, GcCellRefMut as RefMut, Trace, -}; +#![warn( + clippy::perf, + clippy::single_match_else, + clippy::dbg_macro, + clippy::doc_markdown, + clippy::wildcard_imports, + clippy::struct_excessive_bools, + clippy::doc_markdown, + clippy::semicolon_if_nothing_returned, + clippy::pedantic +)] +#![deny( + clippy::all, + clippy::cast_lossless, + clippy::redundant_closure_for_method_calls, + clippy::use_self, + clippy::unnested_or_patterns, + clippy::trivially_copy_pass_by_ref, + clippy::needless_pass_by_value, + clippy::match_wildcard_for_single_variants, + clippy::map_unwrap_or, + clippy::undocumented_unsafe_blocks, + clippy::missing_safety_doc, + unsafe_op_in_unsafe_fn, + unused_qualifications, + unused_import_braces, + unused_lifetimes, + unreachable_pub, + trivial_numeric_casts, + rustdoc::broken_intra_doc_links, + missing_debug_implementations, + missing_copy_implementations, + deprecated_in_future, + meta_variable_misuse, + non_ascii_idents, + rust_2018_compatibility, + rust_2018_idioms, + future_incompatible, + nonstandard_style, + missing_docs +)] +#![allow(clippy::let_unit_value, clippy::module_name_repetitions)] + +extern crate self as boa_gc; + +use boa_profiler::Profiler; +use std::cell::{Cell, RefCell}; +use std::mem; +use std::ptr::NonNull; + +mod trace; + +pub(crate) mod internals; + +mod cell; +mod pointers; + +pub use crate::trace::{Finalize, Trace}; +pub use boa_macros::{Finalize, Trace}; +pub use cell::{GcCell, GcCellRef, GcCellRefMut}; +pub use pointers::{Ephemeron, Gc, WeakGc}; + +use internals::GcBox; + +type GcPointer = NonNull>; + +thread_local!(static EPHEMERON_QUEUE: Cell>> = Cell::new(None)); +thread_local!(static GC_DROPPING: Cell = Cell::new(false)); +thread_local!(static BOA_GC: RefCell = RefCell::new( BoaGc { + config: GcConfig::default(), + runtime: GcRuntimeData::default(), + adult_start: Cell::new(None), +})); + +#[derive(Debug, Clone, Copy)] +struct GcConfig { + threshold: usize, + used_space_percentage: usize, +} + +// Setting the defaults to an arbitrary value currently. +// +// TODO: Add a configure later +impl Default for GcConfig { + fn default() -> Self { + Self { + threshold: 1024, + used_space_percentage: 80, + } + } +} + +#[derive(Default, Debug, Clone, Copy)] +struct GcRuntimeData { + collections: usize, + bytes_allocated: usize, +} + +#[derive(Debug)] +struct BoaGc { + config: GcConfig, + runtime: GcRuntimeData, + adult_start: Cell>, +} + +impl Drop for BoaGc { + fn drop(&mut self) { + Collector::dump(self); + } +} + +// Whether or not the thread is currently in the sweep phase of garbage collection. +// During this phase, attempts to dereference a `Gc` pointer will trigger a panic. +/// `DropGuard` flags whether the Collector is currently running `Collector::sweep()` or `Collector::dump()` +/// +/// While the `DropGuard` is active, all `GcBox`s must not be dereferenced or accessed as it could cause Undefined Behavior +#[derive(Debug, Clone)] +struct DropGuard; + +impl DropGuard { + fn new() -> Self { + GC_DROPPING.with(|dropping| dropping.set(true)); + Self + } +} + +impl Drop for DropGuard { + fn drop(&mut self) { + GC_DROPPING.with(|dropping| dropping.set(false)); + } +} + +/// Returns `true` if it is safe for a type to run [`Finalize::finalize`]. +#[must_use] +#[inline] +pub fn finalizer_safe() -> bool { + GC_DROPPING.with(|dropping| !dropping.get()) +} + +/// The Allocator handles allocation of garbage collected values. +/// +/// The allocator can trigger a garbage collection. +#[derive(Debug, Clone, Copy)] +struct Allocator; + +impl Allocator { + /// Allocate a new garbage collected value to the Garbage Collector's heap. + fn allocate(value: GcBox) -> NonNull> { + let _timer = Profiler::global().start_event("New Pointer", "BoaAlloc"); + let element_size = mem::size_of_val::>(&value); + BOA_GC.with(|st| { + let mut gc = st.borrow_mut(); + + Self::manage_state(&mut gc); + value.header.next.set(gc.adult_start.take()); + // Safety: Value Cannot be a null as it must be a GcBox + let ptr = unsafe { NonNull::new_unchecked(Box::into_raw(Box::from(value))) }; + + gc.adult_start.set(Some(ptr)); + gc.runtime.bytes_allocated += element_size; + + ptr + }) + } + + fn manage_state(gc: &mut BoaGc) { + if gc.runtime.bytes_allocated > gc.config.threshold { + Collector::run_full_collection(gc); + + if gc.runtime.bytes_allocated + > gc.config.threshold / 100 * gc.config.used_space_percentage + { + gc.config.threshold = + gc.runtime.bytes_allocated / gc.config.used_space_percentage * 100; + } + } + } +} + +// This collector currently functions in four main phases +// +// Mark -> Finalize -> Mark -> Sweep +// +// Mark nodes as reachable then finalize the unreachable nodes. A remark phase +// then needs to be retriggered as finalization can potentially resurrect dead +// nodes. +// +// A better approach in a more concurrent structure may be to reorder. +// +// Mark -> Sweep -> Finalize +struct Collector; + +impl Collector { + /// Run a collection on the full heap. + fn run_full_collection(gc: &mut BoaGc) { + let _timer = Profiler::global().start_event("Gc Full Collection", "gc"); + gc.runtime.collections += 1; + let unreachable_adults = Self::mark_heap(&gc.adult_start); + + // Check if any unreachable nodes were found and finalize + if !unreachable_adults.is_empty() { + // SAFETY: Please see `Collector::finalize()` + unsafe { Self::finalize(unreachable_adults) }; + } + + let _final_unreachable_adults = Self::mark_heap(&gc.adult_start); + + // SAFETY: Please see `Collector::sweep()` + unsafe { + Self::sweep(&gc.adult_start, &mut gc.runtime.bytes_allocated); + } + } + + /// Walk the heap and mark any nodes deemed reachable + fn mark_heap(head: &Cell>>>) -> Vec>> { + let _timer = Profiler::global().start_event("Gc Marking", "gc"); + // Walk the list, tracing and marking the nodes + let mut finalize = Vec::new(); + let mut ephemeron_queue = Vec::new(); + let mut mark_head = head; + while let Some(node) = mark_head.get() { + // SAFETY: node must be valid as it is coming directly from the heap. + let node_ref = unsafe { node.as_ref() }; + if node_ref.header.is_ephemeron() { + ephemeron_queue.push(node); + } else if node_ref.header.roots() > 0 { + // SAFETY: the reference to node must be valid as it is rooted. Passing + // invalid references can result in Undefined Behavior + unsafe { + node_ref.trace_inner(); + } + } else { + finalize.push(node); + } + mark_head = &node_ref.header.next; + } + + // Ephemeron Evaluation + if !ephemeron_queue.is_empty() { + ephemeron_queue = Self::mark_ephemerons(ephemeron_queue); + } + + // Any left over nodes in the ephemeron queue at this point are + // unreachable and need to be notified/finalized. + finalize.extend(ephemeron_queue); + + finalize + } + + // Tracing Ephemerons/Weak is always requires tracing the inner nodes in case it ends up marking unmarked node + // + // Time complexity should be something like O(nd) where d is the longest chain of epehemerons + /// Mark any ephemerons that are deemed live and trace their fields. + fn mark_ephemerons( + initial_queue: Vec>>, + ) -> Vec>> { + let mut ephemeron_queue = initial_queue; + loop { + // iterate through ephemeron queue, sorting nodes by whether they + // are reachable or unreachable + let (reachable, other): (Vec<_>, Vec<_>) = + ephemeron_queue.into_iter().partition(|node| { + // SAFETY: Any node on the eph_queue or the heap must be non null + let node = unsafe { node.as_ref() }; + if node.value.is_marked_ephemeron() { + node.header.mark(); + true + } else { + node.header.roots() > 0 + } + }); + // Replace the old queue with the unreachable + ephemeron_queue = other; + + // If reachable nodes is not empty, trace values. If it is empty, + // break from the loop + if reachable.is_empty() { + break; + } + EPHEMERON_QUEUE.with(|state| state.set(Some(Vec::new()))); + // iterate through reachable nodes and trace their values, + // enqueuing any ephemeron that is found during the trace + for node in reachable { + // TODO: deal with fetch ephemeron_queue + // SAFETY: Node must be a valid pointer or else it would not be deemed reachable. + unsafe { + node.as_ref().weak_trace_inner(); + } + } + + EPHEMERON_QUEUE.with(|st| { + if let Some(found_nodes) = st.take() { + ephemeron_queue.extend(found_nodes); + } + }); + } + ephemeron_queue + } + + /// # Safety + /// + /// Passing a vec with invalid pointers will result in Undefined Behaviour. + unsafe fn finalize(finalize_vec: Vec>>) { + let _timer = Profiler::global().start_event("Gc Finalization", "gc"); + for node in finalize_vec { + // We double check that the unreachable nodes are actually unreachable + // prior to finalization as they could have been marked by a different + // trace after initially being added to the queue + // + // SAFETY: The caller must ensure all pointers inside `finalize_vec` are valid. + let node = unsafe { node.as_ref() }; + if !node.header.is_marked() { + Trace::run_finalizer(&node.value); + } + } + } + + /// # Safety + /// + /// - Providing an invalid pointer in the `heap_start` or in any of the headers of each + /// node will result in Undefined Behaviour. + /// - Providing a list of pointers that weren't allocated by `Box::into_raw(Box::new(..))` + /// will result in Undefined Behaviour. + unsafe fn sweep( + heap_start: &Cell>>>, + total_allocated: &mut usize, + ) { + let _timer = Profiler::global().start_event("Gc Sweeping", "gc"); + let _guard = DropGuard::new(); + + let mut sweep_head = heap_start; + while let Some(node) = sweep_head.get() { + // SAFETY: The caller must ensure the validity of every node of `heap_start`. + let node_ref = unsafe { node.as_ref() }; + if node_ref.is_marked() { + node_ref.header.unmark(); + sweep_head = &node_ref.header.next; + } else if node_ref.header.is_ephemeron() && node_ref.header.roots() > 0 { + // Keep the ephemeron box's alive if rooted, but note that it's pointer is no longer safe + Trace::run_finalizer(&node_ref.value); + sweep_head = &node_ref.header.next; + } else { + // SAFETY: The algorithm ensures only unmarked/unreachable pointers are dropped. + // The caller must ensure all pointers were allocated by `Box::into_raw(Box::new(..))`. + let unmarked_node = unsafe { Box::from_raw(node.as_ptr()) }; + let unallocated_bytes = mem::size_of_val::>(&*unmarked_node); + *total_allocated -= unallocated_bytes; + sweep_head.set(unmarked_node.header.next.take()); + } + } + } + + // Clean up the heap when BoaGc is dropped + fn dump(gc: &mut BoaGc) { + // Not initializing a dropguard since this should only be invoked when BOA_GC is being dropped. + let _guard = DropGuard::new(); + + let sweep_head = &gc.adult_start; + while let Some(node) = sweep_head.get() { + // SAFETY: + // The `Allocator` must always ensure its start node is a valid, non-null pointer that + // was allocated by `Box::from_raw(Box::new(..))`. + let unmarked_node = unsafe { Box::from_raw(node.as_ptr()) }; + sweep_head.set(unmarked_node.header.next.take()); + } + } +} + +/// Forcefully runs a garbage collection of all unaccessible nodes. +pub fn force_collect() { + BOA_GC.with(|current| { + let mut gc = current.borrow_mut(); + + if gc.runtime.bytes_allocated > 0 { + Collector::run_full_collection(&mut gc); + } + }); +} + +#[cfg(test)] +mod test; diff --git a/boa_gc/src/pointers/ephemeron.rs b/boa_gc/src/pointers/ephemeron.rs new file mode 100644 index 00000000000..35702a7b119 --- /dev/null +++ b/boa_gc/src/pointers/ephemeron.rs @@ -0,0 +1,125 @@ +use crate::{ + finalizer_safe, + internals::EphemeronBox, + trace::{Finalize, Trace}, + Allocator, Gc, GcBox, EPHEMERON_QUEUE, +}; +use std::cell::Cell; +use std::ptr::NonNull; + +#[derive(Debug)] +/// A key-value pair where the value becomes unaccesible when the key is garbage collected. +/// +/// See Racket's explanation on [**ephemerons**][eph] for a brief overview or read Barry Hayes' +/// [_Ephemerons_: a new finalization mechanism][acm]. +/// +/// +/// [eph]: https://docs.racket-lang.org/reference/ephemerons.html +/// [acm]: https://dl.acm.org/doi/10.1145/263700.263733 +pub struct Ephemeron { + inner_ptr: Cell>>>, +} + +impl Ephemeron { + /// Creates a new `Ephemeron`. + pub fn new(key: &Gc, value: V) -> Self { + Self { + inner_ptr: Cell::new(Allocator::allocate(GcBox::new_weak(EphemeronBox::new( + key, value, + )))), + } + } +} + +impl Ephemeron { + #[inline] + fn inner_ptr(&self) -> NonNull>> { + self.inner_ptr.get() + } + + #[inline] + fn inner(&self) -> &GcBox> { + // SAFETY: GcBox> must live until it is unrooted by Drop + unsafe { &*self.inner_ptr().as_ptr() } + } + + #[inline] + /// Gets the weak key of this `Ephemeron`, or `None` if the key was already garbage + /// collected. + pub fn key(&self) -> Option<&K> { + self.inner().value().key() + } + + #[inline] + /// Gets the stored value of this `Ephemeron`. + pub fn value(&self) -> &V { + self.inner().value().value() + } + + #[inline] + /// Gets a `Gc` for the stored key of this `Ephemeron`. + pub fn upgrade_key(&self) -> Option> { + // SAFETY: ptr must be a valid pointer or None would have been returned. + self.inner().value().inner_key_ptr().map(|ptr| unsafe { + let inner_ptr = NonNull::new_unchecked(ptr); + Gc::from_ptr(inner_ptr) + }) + } +} + +impl Finalize for Ephemeron {} + +// SAFETY: Ephemerons trace implementation is standard for everything except `Trace::weak_trace()`, +// which pushes the GcBox> onto the EphemeronQueue +unsafe impl Trace for Ephemeron { + #[inline] + unsafe fn trace(&self) {} + + // Push this Ephemeron's pointer onto the EphemeronQueue + #[inline] + unsafe fn weak_trace(&self) { + EPHEMERON_QUEUE.with(|q| { + let mut queue = q.take().expect("queue is initialized by weak_trace"); + queue.push(self.inner_ptr()); + }); + } + + #[inline] + unsafe fn root(&self) {} + + #[inline] + unsafe fn unroot(&self) {} + + #[inline] + fn run_finalizer(&self) { + Finalize::finalize(self); + } +} + +impl Clone for Ephemeron { + #[inline] + fn clone(&self) -> Self { + // SAFETY: This is safe because the inner_ptr must live as long as it's roots. + // Mismanagement of roots can cause inner_ptr to use after free or Undefined + // Behavior. + unsafe { + let eph = Self { + inner_ptr: Cell::new(NonNull::new_unchecked(self.inner_ptr().as_ptr())), + }; + // Increment the Ephemeron's GcBox roots by 1 + self.inner().root_inner(); + eph + } + } +} + +impl Drop for Ephemeron { + #[inline] + fn drop(&mut self) { + // NOTE: We assert that this drop call is not a + // drop from `Collector::dump` or `Collector::sweep` + if finalizer_safe() { + self.inner().unroot_inner(); + } + } +} diff --git a/boa_gc/src/pointers/gc.rs b/boa_gc/src/pointers/gc.rs new file mode 100644 index 00000000000..df44a8b327e --- /dev/null +++ b/boa_gc/src/pointers/gc.rs @@ -0,0 +1,275 @@ +use std::cell::Cell; +use std::cmp::Ordering; +use std::fmt::{self, Debug, Display}; +use std::hash::{Hash, Hasher}; +use std::marker::PhantomData; +use std::ops::Deref; +use std::ptr::{self, addr_of_mut, NonNull}; +use std::rc::Rc; + +use crate::internals::GcBox; +use crate::trace::{Finalize, Trace}; +use crate::{finalizer_safe, Allocator}; + +// Technically, this function is safe, since we're just modifying the address of a pointer without +// dereferencing it. +pub(crate) fn set_data_ptr(mut ptr: *mut T, data: *mut U) -> *mut T { + // SAFETY: this should be safe as ptr must be a valid nonnull + unsafe { + ptr::write(addr_of_mut!(ptr).cast::<*mut u8>(), data.cast::()); + } + ptr +} + +/// A garbage-collected pointer type over an immutable value. +pub struct Gc { + pub(crate) inner_ptr: Cell>>, + pub(crate) marker: PhantomData>, +} + +impl Gc { + /// Constructs a new `Gc` with the given value. + pub fn new(value: T) -> Self { + // Create GcBox and allocate it to heap. + // + // Note: Allocator can cause Collector to run + let inner_ptr = Allocator::allocate(GcBox::new(value)); + // SAFETY: inner_ptr was just allocated, so it must be a valid value that implements [`Trace`] + unsafe { (*inner_ptr.as_ptr()).value().unroot() } + let gc = Self { + inner_ptr: Cell::new(inner_ptr), + marker: PhantomData, + }; + gc.set_root(); + gc + } +} + +impl Gc { + /// Returns `true` if the two `Gc`s point to the same allocation. + pub fn ptr_eq(this: &Self, other: &Self) -> bool { + GcBox::ptr_eq(this.inner(), other.inner()) + } + + /// Will return a new rooted `Gc` from a `GcBox` pointer + pub(crate) fn from_ptr(ptr: NonNull>) -> Self { + // SAFETY: the value provided as a pointer MUST be a valid GcBox. + unsafe { + ptr.as_ref().root_inner(); + let gc = Self { + inner_ptr: Cell::new(ptr), + marker: PhantomData, + }; + gc.set_root(); + gc + } + } +} + +/// Returns the given pointer with its root bit cleared. +pub(crate) unsafe fn clear_root_bit( + ptr: NonNull>, +) -> NonNull> { + let ptr = ptr.as_ptr(); + let data = ptr.cast::(); + let addr = data as isize; + let ptr = set_data_ptr(ptr, data.wrapping_offset((addr & !1) - addr)); + // SAFETY: ptr must be a non null value + unsafe { NonNull::new_unchecked(ptr) } +} + +impl Gc { + fn rooted(&self) -> bool { + self.inner_ptr.get().as_ptr().cast::() as usize & 1 != 0 + } + + pub(crate) fn set_root(&self) { + let ptr = self.inner_ptr.get().as_ptr(); + let data = ptr.cast::(); + let addr = data as isize; + let ptr = set_data_ptr(ptr, data.wrapping_offset((addr | 1) - addr)); + // SAFETY: ptr must be a non null value. + unsafe { + self.inner_ptr.set(NonNull::new_unchecked(ptr)); + } + } + + fn clear_root(&self) { + // SAFETY: inner_ptr must be a valid non-null pointer to a live GcBox. + unsafe { + self.inner_ptr.set(clear_root_bit(self.inner_ptr.get())); + } + } + + #[inline] + pub(crate) fn inner_ptr(&self) -> NonNull> { + assert!(finalizer_safe()); + // SAFETY: inner_ptr must be a live GcBox. Calling this on a dropped GcBox + // can result in Undefined Behavior. + unsafe { clear_root_bit(self.inner_ptr.get()) } + } + + #[inline] + fn inner(&self) -> &GcBox { + // SAFETY: Please see Gc::inner_ptr() + unsafe { self.inner_ptr().as_ref() } + } +} + +impl Finalize for Gc {} + +// SAFETY: `Gc` maintains it's own rootedness and implements all methods of +// Trace. It is not possible to root an already rooted `Gc` and vice versa. +unsafe impl Trace for Gc { + #[inline] + unsafe fn trace(&self) { + // SAFETY: Inner must be live and allocated GcBox. + unsafe { + self.inner().trace_inner(); + } + } + + #[inline] + unsafe fn weak_trace(&self) { + self.inner().weak_trace_inner(); + } + + #[inline] + unsafe fn root(&self) { + assert!(!self.rooted(), "Can't double-root a Gc"); + // Try to get inner before modifying our state. Inner may be + // inaccessible due to this method being invoked during the sweeping + // phase, and we don't want to modify our state before panicking. + self.inner().root_inner(); + self.set_root(); + } + + #[inline] + unsafe fn unroot(&self) { + assert!(self.rooted(), "Can't double-unroot a Gc"); + // Try to get inner before modifying our state. Inner may be + // inaccessible due to this method being invoked during the sweeping + // phase, and we don't want to modify our state before panicking. + self.inner().unroot_inner(); + self.clear_root(); + } + + #[inline] + fn run_finalizer(&self) { + Finalize::finalize(self); + } +} + +impl Clone for Gc { + #[inline] + fn clone(&self) -> Self { + Self::from_ptr(self.inner_ptr()) + } +} + +impl Deref for Gc { + type Target = T; + + #[inline] + fn deref(&self) -> &T { + self.inner().value() + } +} + +impl Drop for Gc { + #[inline] + fn drop(&mut self) { + // If this pointer was a root, we should unroot it. + if self.rooted() { + self.inner().unroot_inner(); + } + } +} + +impl Default for Gc { + #[inline] + fn default() -> Self { + Self::new(Default::default()) + } +} + +#[allow(clippy::inline_always)] +impl PartialEq for Gc { + #[inline(always)] + fn eq(&self, other: &Self) -> bool { + **self == **other + } +} + +impl Eq for Gc {} + +#[allow(clippy::inline_always)] +impl PartialOrd for Gc { + #[inline(always)] + fn partial_cmp(&self, other: &Self) -> Option { + (**self).partial_cmp(&**other) + } + + #[inline(always)] + fn lt(&self, other: &Self) -> bool { + **self < **other + } + + #[inline(always)] + fn le(&self, other: &Self) -> bool { + **self <= **other + } + + #[inline(always)] + fn gt(&self, other: &Self) -> bool { + **self > **other + } + + #[inline(always)] + fn ge(&self, other: &Self) -> bool { + **self >= **other + } +} + +impl Ord for Gc { + #[inline] + fn cmp(&self, other: &Self) -> Ordering { + (**self).cmp(&**other) + } +} + +impl Hash for Gc { + fn hash(&self, state: &mut H) { + (**self).hash(state); + } +} + +impl Display for Gc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Display::fmt(&**self, f) + } +} + +impl Debug for Gc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + Debug::fmt(&**self, f) + } +} + +impl fmt::Pointer for Gc { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Pointer::fmt(&self.inner(), f) + } +} + +impl std::borrow::Borrow for Gc { + fn borrow(&self) -> &T { + self + } +} + +impl AsRef for Gc { + fn as_ref(&self) -> &T { + self + } +} diff --git a/boa_gc/src/pointers/mod.rs b/boa_gc/src/pointers/mod.rs new file mode 100644 index 00000000000..5c14182762d --- /dev/null +++ b/boa_gc/src/pointers/mod.rs @@ -0,0 +1,9 @@ +//! Pointers represents the External types returned by the Boa Garbage Collector + +mod ephemeron; +mod gc; +mod weak; + +pub use ephemeron::Ephemeron; +pub use gc::Gc; +pub use weak::WeakGc; diff --git a/boa_gc/src/pointers/weak.rs b/boa_gc/src/pointers/weak.rs new file mode 100644 index 00000000000..26a1945efa2 --- /dev/null +++ b/boa_gc/src/pointers/weak.rs @@ -0,0 +1,49 @@ +use crate::{Ephemeron, Finalize, Gc, Trace}; + +/// A weak reference to a [`Gc`]. +/// +/// This type allows keeping references to [`Gc`] managed values without keeping them alive for +/// garbage collections. However, this also means [`WeakGc::value`] can return `None` at any moment. +#[derive(Debug, Trace, Finalize)] +#[repr(transparent)] +pub struct WeakGc { + inner: Ephemeron, +} + +impl WeakGc { + /// Creates a new weak pointer for a garbage collected value. + pub fn new(value: &Gc) -> Self { + Self { + inner: Ephemeron::new(value, ()), + } + } +} + +impl WeakGc { + #[inline] + /// Gets the value of this weak pointer, or `None` if the value was already garbage collected. + pub fn value(&self) -> Option<&T> { + self.inner.key() + } + + #[inline] + /// Upgrade returns a `Gc` pointer for the internal value if valid, or None if the value was already garbage collected. + pub fn upgrade(&self) -> Option> { + self.inner.upgrade_key() + } +} + +impl Clone for WeakGc { + #[inline] + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + } + } +} + +impl From> for WeakGc { + fn from(inner: Ephemeron) -> Self { + Self { inner } + } +} diff --git a/boa_gc/src/test/allocation.rs b/boa_gc/src/test/allocation.rs new file mode 100644 index 00000000000..27386999836 --- /dev/null +++ b/boa_gc/src/test/allocation.rs @@ -0,0 +1,31 @@ +use super::{run_test, Harness}; +use crate::{force_collect, Gc, GcCell}; + +#[test] +fn gc_basic_cell_allocation() { + run_test(|| { + let gc_cell = Gc::new(GcCell::new(16_u16)); + + force_collect(); + Harness::assert_collections(1); + Harness::assert_bytes_allocated(); + assert_eq!(*gc_cell.borrow_mut(), 16); + }); +} + +#[test] +fn gc_basic_pointer_alloc() { + run_test(|| { + let gc = Gc::new(16_u8); + + force_collect(); + Harness::assert_collections(1); + Harness::assert_bytes_allocated(); + assert_eq!(*gc, 16); + + drop(gc); + force_collect(); + Harness::assert_collections(2); + Harness::assert_empty_gc(); + }); +} diff --git a/boa_gc/src/test/cell.rs b/boa_gc/src/test/cell.rs new file mode 100644 index 00000000000..1e82acf1e7c --- /dev/null +++ b/boa_gc/src/test/cell.rs @@ -0,0 +1,15 @@ +use boa_gc::{Gc, GcCell}; + +use super::run_test; + +#[test] +fn boa_borrow_mut_test() { + run_test(|| { + let v = Gc::new(GcCell::new(Vec::new())); + + for _ in 1..=259 { + let cell = Gc::new(GcCell::new([0u8; 10])); + v.borrow_mut().push(cell); + } + }); +} diff --git a/boa_gc/src/test/mod.rs b/boa_gc/src/test/mod.rs new file mode 100644 index 00000000000..559fbb8d80f --- /dev/null +++ b/boa_gc/src/test/mod.rs @@ -0,0 +1,37 @@ +use crate::BOA_GC; + +mod allocation; +mod cell; +mod weak; + +struct Harness; + +impl Harness { + fn assert_collections(o: usize) { + BOA_GC.with(|current| { + let gc = current.borrow(); + assert_eq!(gc.runtime.collections, o); + }); + } + + fn assert_empty_gc() { + BOA_GC.with(|current| { + let gc = current.borrow(); + + assert!(gc.adult_start.get().is_none()); + assert!(gc.runtime.bytes_allocated == 0); + }); + } + + fn assert_bytes_allocated() { + BOA_GC.with(|current| { + let gc = current.borrow(); + assert!(gc.runtime.bytes_allocated > 0); + }); + } +} + +fn run_test(test: impl FnOnce() + Send + 'static) { + let handle = std::thread::spawn(test); + handle.join().unwrap(); +} diff --git a/boa_gc/src/test/weak.rs b/boa_gc/src/test/weak.rs new file mode 100644 index 00000000000..5f1e33ccc19 --- /dev/null +++ b/boa_gc/src/test/weak.rs @@ -0,0 +1,133 @@ +use boa_gc::{force_collect, Ephemeron, Gc, WeakGc}; + +use super::run_test; + +#[test] +fn eph_weak_gc_test() { + run_test(|| { + let gc_value = Gc::new(3); + + { + let cloned_gc = gc_value.clone(); + + let weak = WeakGc::new(&cloned_gc); + + assert_eq!(*weak.value().expect("Is live currently"), 3); + drop(cloned_gc); + force_collect(); + assert_eq!(*weak.value().expect("WeakGc is still live here"), 3); + + drop(gc_value); + force_collect(); + + assert!(weak.value().is_none()); + } + }); +} + +#[test] +fn eph_ephemeron_test() { + run_test(|| { + let gc_value = Gc::new(3); + + { + let cloned_gc = gc_value.clone(); + + let ephemeron = Ephemeron::new(&cloned_gc, String::from("Hello World!")); + + assert_eq!(*ephemeron.key().expect("Ephemeron is live"), 3); + assert_eq!(*ephemeron.value(), String::from("Hello World!")); + drop(cloned_gc); + force_collect(); + assert_eq!(*ephemeron.key().expect("Ephemeron is still live here"), 3); + + drop(gc_value); + force_collect(); + + assert!(ephemeron.key().is_none()); + } + }); +} + +#[test] +fn eph_allocation_chains() { + run_test(|| { + let gc_value = Gc::new(String::from("foo")); + + { + let cloned_gc = gc_value.clone(); + let weak = WeakGc::new(&cloned_gc); + let wrap = Gc::new(weak); + + assert_eq!(wrap.value().expect("weak is live"), &String::from("foo")); + + let eph = Ephemeron::new(&wrap, 3); + + drop(cloned_gc); + force_collect(); + + assert_eq!( + eph.key() + .expect("eph is still live") + .value() + .expect("weak is still live"), + &String::from("foo") + ); + + drop(gc_value); + force_collect(); + + assert!(eph.key().expect("eph is still live").value().is_none()); + } + }); +} + +#[test] +fn eph_basic_alloc_dump_test() { + run_test(|| { + let gc_value = Gc::new(String::from("gc here")); + let _gc_two = Gc::new("hmmm"); + + let eph = Ephemeron::new(&gc_value, 4); + let _fourth = Gc::new("tail"); + + assert_eq!(*eph.key().expect("must be live"), String::from("gc here")); + }); +} + +#[test] +fn eph_basic_upgrade_test() { + run_test(|| { + let init_gc = Gc::new(String::from("foo")); + + let weak = WeakGc::new(&init_gc); + + let new_gc = weak.upgrade().expect("Weak is still live"); + + drop(weak); + force_collect(); + + assert_eq!(*init_gc, *new_gc); + }); +} + +#[test] +fn eph_basic_clone_test() { + run_test(|| { + let init_gc = Gc::new(String::from("bar")); + + let weak = WeakGc::new(&init_gc); + + let new_gc = weak.upgrade().expect("Weak is live"); + let new_weak = weak.clone(); + + drop(weak); + force_collect(); + + assert_eq!(*new_gc, *new_weak.value().expect("weak should be live")); + assert_eq!( + *init_gc, + *new_weak.value().expect("weak_should be live still") + ); + }); +} diff --git a/boa_gc/src/trace.rs b/boa_gc/src/trace.rs new file mode 100644 index 00000000000..354a0119d40 --- /dev/null +++ b/boa_gc/src/trace.rs @@ -0,0 +1,450 @@ +use std::borrow::{Cow, ToOwned}; +use std::collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, LinkedList, VecDeque}; +use std::hash::{BuildHasher, Hash}; +use std::marker::PhantomData; +use std::num::{ + NonZeroI128, NonZeroI16, NonZeroI32, NonZeroI64, NonZeroI8, NonZeroIsize, NonZeroU128, + NonZeroU16, NonZeroU32, NonZeroU64, NonZeroU8, NonZeroUsize, +}; +use std::path::{Path, PathBuf}; +use std::rc::Rc; +use std::sync::atomic::{ + AtomicBool, AtomicI16, AtomicI32, AtomicI64, AtomicI8, AtomicIsize, AtomicU16, AtomicU32, + AtomicU64, AtomicU8, AtomicUsize, +}; + +/// Substitute for the [`Drop`] trait for garbage collected types. +pub trait Finalize { + /// Cleanup logic for a type. + fn finalize(&self) {} +} + +/// The Trace trait, which needs to be implemented on garbage-collected objects. +/// +/// # Safety +/// +/// - An incorrect implementation of the trait can result in heap overflows, data corruption, +/// use-after-free, or Undefined Behaviour in general. +/// +/// - Calling any of the functions marked as `unsafe` outside of the context of the garbage collector +/// can result in Undefined Behaviour. +pub unsafe trait Trace: Finalize { + /// Marks all contained `Gc`s. + /// + /// # Safety + /// + /// See [`Trace`]. + unsafe fn trace(&self); + + /// Marks all contained weak references of a `Gc`. + /// + /// # Safety + /// + /// See [`Trace`]. + unsafe fn weak_trace(&self); + + /// Increments the root-count of all contained `Gc`s. + /// + /// # Safety + /// + /// See [`Trace`]. + unsafe fn root(&self); + + /// Decrements the root-count of all contained `Gc`s. + /// + /// # Safety + /// + /// See [`Trace`]. + unsafe fn unroot(&self); + + /// Checks if an ephemeron's key is marked. + #[doc(hidden)] + fn is_marked_ephemeron(&self) -> bool { + false + } + + /// Runs [`Finalize::finalize`] on this object and all + /// contained subobjects. + fn run_finalizer(&self); +} + +/// Utility macro to define an empty implementation of [`Trace`]. +/// +/// Use this for marking types as not containing any `Trace` types. +#[macro_export] +macro_rules! empty_trace { + () => { + #[inline] + unsafe fn trace(&self) {} + #[inline] + unsafe fn weak_trace(&self) {} + #[inline] + unsafe fn root(&self) {} + #[inline] + unsafe fn unroot(&self) {} + #[inline] + fn run_finalizer(&self) { + $crate::Finalize::finalize(self) + } + }; +} + +/// Utility macro to manually implement [`Trace`] on a type. +/// +/// You define a `this` parameter name and pass in a body, which should call `mark` on every +/// traceable element inside the body. The mark implementation will automatically delegate to the +/// correct method on the argument. +/// +/// # Safety +/// +/// Misusing the `mark` function may result in Undefined Behaviour. +#[macro_export] +macro_rules! custom_trace { + ($this:ident, $body:expr) => { + #[inline] + unsafe fn trace(&self) { + #[inline] + fn mark(it: &T) { + // SAFETY: The implementor must ensure that `trace` is correctly implemented. + unsafe { + $crate::Trace::trace(it); + } + } + let $this = self; + $body + } + #[inline] + unsafe fn weak_trace(&self) { + #[inline] + fn mark(it: &T) { + // SAFETY: The implementor must ensure that `weak_trace` is correctly implemented. + unsafe { + $crate::Trace::weak_trace(it); + } + } + let $this = self; + $body + } + #[inline] + unsafe fn root(&self) { + #[inline] + fn mark(it: &T) { + // SAFETY: The implementor must ensure that `root` is correctly implemented. + unsafe { + $crate::Trace::root(it); + } + } + let $this = self; + $body + } + #[inline] + unsafe fn unroot(&self) { + #[inline] + fn mark(it: &T) { + // SAFETY: The implementor must ensure that `unroot` is correctly implemented. + unsafe { + $crate::Trace::unroot(it); + } + } + let $this = self; + $body + } + #[inline] + fn run_finalizer(&self) { + #[inline] + fn mark(it: &T) { + $crate::Trace::run_finalizer(it); + } + $crate::Finalize::finalize(self); + let $this = self; + $body + } + }; +} + +impl Finalize for &'static T {} +// SAFETY: 'static references don't need to be traced, since they live indefinitely. +unsafe impl Trace for &'static T { + empty_trace!(); +} + +macro_rules! simple_empty_finalize_trace { + ($($T:ty),*) => { + $( + impl Finalize for $T {} + + // SAFETY: + // Primitive types and string types don't have inner nodes that need to be marked. + unsafe impl Trace for $T { empty_trace!(); } + )* + } +} + +simple_empty_finalize_trace![ + (), + bool, + isize, + usize, + i8, + u8, + i16, + u16, + i32, + u32, + i64, + u64, + i128, + u128, + f32, + f64, + char, + String, + Box, + Rc, + Path, + PathBuf, + NonZeroIsize, + NonZeroUsize, + NonZeroI8, + NonZeroU8, + NonZeroI16, + NonZeroU16, + NonZeroI32, + NonZeroU32, + NonZeroI64, + NonZeroU64, + NonZeroI128, + NonZeroU128, + AtomicBool, + AtomicIsize, + AtomicUsize, + AtomicI8, + AtomicU8, + AtomicI16, + AtomicU16, + AtomicI32, + AtomicU32, + AtomicI64, + AtomicU64 +]; + +impl Finalize for [T; N] {} +// SAFETY: +// All elements inside the array are correctly marked. +unsafe impl Trace for [T; N] { + custom_trace!(this, { + for v in this { + mark(v); + } + }); +} + +macro_rules! fn_finalize_trace_one { + ($ty:ty $(,$args:ident)*) => { + impl Finalize for $ty {} + // SAFETY: + // Function pointers don't have inner nodes that need to be marked. + unsafe impl Trace for $ty { empty_trace!(); } + } +} +macro_rules! fn_finalize_trace_group { + () => { + fn_finalize_trace_one!(extern "Rust" fn () -> Ret); + fn_finalize_trace_one!(extern "C" fn () -> Ret); + fn_finalize_trace_one!(unsafe extern "Rust" fn () -> Ret); + fn_finalize_trace_one!(unsafe extern "C" fn () -> Ret); + }; + ($($args:ident),*) => { + fn_finalize_trace_one!(extern "Rust" fn ($($args),*) -> Ret, $($args),*); + fn_finalize_trace_one!(extern "C" fn ($($args),*) -> Ret, $($args),*); + fn_finalize_trace_one!(extern "C" fn ($($args),*, ...) -> Ret, $($args),*); + fn_finalize_trace_one!(unsafe extern "Rust" fn ($($args),*) -> Ret, $($args),*); + fn_finalize_trace_one!(unsafe extern "C" fn ($($args),*) -> Ret, $($args),*); + fn_finalize_trace_one!(unsafe extern "C" fn ($($args),*, ...) -> Ret, $($args),*); + } +} + +macro_rules! tuple_finalize_trace { + () => {}; // This case is handled above, by simple_finalize_empty_trace!(). + ($($args:ident),*) => { + impl<$($args),*> Finalize for ($($args,)*) {} + // SAFETY: + // All elements inside the tuple are correctly marked. + unsafe impl<$($args: $crate::Trace),*> Trace for ($($args,)*) { + custom_trace!(this, { + #[allow(non_snake_case, unused_unsafe)] + fn avoid_lints<$($args: $crate::Trace),*>(&($(ref $args,)*): &($($args,)*)) { + // SAFETY: The implementor must ensure a correct implementation. + unsafe { $(mark($args);)* } + } + avoid_lints(this) + }); + } + } +} + +macro_rules! type_arg_tuple_based_finalize_trace_impls { + ($(($($args:ident),*);)*) => { + $( + fn_finalize_trace_group!($($args),*); + tuple_finalize_trace!($($args),*); + )* + } +} + +type_arg_tuple_based_finalize_trace_impls![ + (); + (A); + (A, B); + (A, B, C); + (A, B, C, D); + (A, B, C, D, E); + (A, B, C, D, E, F); + (A, B, C, D, E, F, G); + (A, B, C, D, E, F, G, H); + (A, B, C, D, E, F, G, H, I); + (A, B, C, D, E, F, G, H, I, J); + (A, B, C, D, E, F, G, H, I, J, K); + (A, B, C, D, E, F, G, H, I, J, K, L); +]; + +impl Finalize for Box {} +// SAFETY: The inner value of the `Box` is correctly marked. +unsafe impl Trace for Box { + custom_trace!(this, { + mark(&**this); + }); +} + +impl Finalize for Box<[T]> {} +// SAFETY: All the inner elements of the `Box` array are correctly marked. +unsafe impl Trace for Box<[T]> { + custom_trace!(this, { + for e in this.iter() { + mark(e); + } + }); +} + +impl Finalize for Vec {} +// SAFETY: All the inner elements of the `Vec` are correctly marked. +unsafe impl Trace for Vec { + custom_trace!(this, { + for e in this { + mark(e); + } + }); +} + +impl Finalize for Option {} +// SAFETY: The inner value of the `Option` is correctly marked. +unsafe impl Trace for Option { + custom_trace!(this, { + if let Some(ref v) = *this { + mark(v); + } + }); +} + +impl Finalize for Result {} +// SAFETY: Both inner values of the `Result` are correctly marked. +unsafe impl Trace for Result { + custom_trace!(this, { + match *this { + Ok(ref v) => mark(v), + Err(ref v) => mark(v), + } + }); +} + +impl Finalize for BinaryHeap {} +// SAFETY: All the elements of the `BinaryHeap` are correctly marked. +unsafe impl Trace for BinaryHeap { + custom_trace!(this, { + for v in this.iter() { + mark(v); + } + }); +} + +impl Finalize for BTreeMap {} +// SAFETY: All the elements of the `BTreeMap` are correctly marked. +unsafe impl Trace for BTreeMap { + custom_trace!(this, { + for (k, v) in this { + mark(k); + mark(v); + } + }); +} + +impl Finalize for BTreeSet {} +// SAFETY: All the elements of the `BTreeSet` are correctly marked. +unsafe impl Trace for BTreeSet { + custom_trace!(this, { + for v in this { + mark(v); + } + }); +} + +impl Finalize for HashMap {} +// SAFETY: All the elements of the `HashMap` are correctly marked. +unsafe impl Trace for HashMap { + custom_trace!(this, { + for (k, v) in this.iter() { + mark(k); + mark(v); + } + }); +} + +impl Finalize for HashSet {} +// SAFETY: All the elements of the `HashSet` are correctly marked. +unsafe impl Trace for HashSet { + custom_trace!(this, { + for v in this.iter() { + mark(v); + } + }); +} + +impl Finalize for LinkedList {} +// SAFETY: All the elements of the `LinkedList` are correctly marked. +unsafe impl Trace for LinkedList { + custom_trace!(this, { + for v in this.iter() { + mark(v); + } + }); +} + +impl Finalize for PhantomData {} +// SAFETY: A `PhantomData` doesn't have inner data that needs to be marked. +unsafe impl Trace for PhantomData { + empty_trace!(); +} + +impl Finalize for VecDeque {} +// SAFETY: All the elements of the `VecDeque` are correctly marked. +unsafe impl Trace for VecDeque { + custom_trace!(this, { + for v in this.iter() { + mark(v); + } + }); +} + +impl Finalize for Cow<'static, T> {} +// SAFETY: 'static references don't need to be traced, since they live indefinitely, and the owned +// variant is correctly marked. +unsafe impl Trace for Cow<'static, T> +where + T::Owned: Trace, +{ + custom_trace!(this, { + if let Cow::Owned(ref v) = this { + mark(v); + } + }); +} diff --git a/boa_macros/Cargo.toml b/boa_macros/Cargo.toml index d68aae7e972..048fd0f0d67 100644 --- a/boa_macros/Cargo.toml +++ b/boa_macros/Cargo.toml @@ -15,4 +15,5 @@ proc-macro = true [dependencies] quote = "1.0.21" syn = "1.0.103" - +proc-macro2 = "1.0" +synstructure = "0.12" diff --git a/boa_macros/src/lib.rs b/boa_macros/src/lib.rs index 205905786c9..c1fc87fa6dd 100644 --- a/boa_macros/src/lib.rs +++ b/boa_macros/src/lib.rs @@ -1,6 +1,7 @@ use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, LitStr}; +use synstructure::{decl_derive, AddBounds, Structure}; /// Construct a utf-16 array literal from a utf-8 [`str`] literal. #[proc_macro] @@ -13,3 +14,103 @@ pub fn utf16(input: TokenStream) -> TokenStream { } .into() } + +decl_derive!([Trace, attributes(unsafe_ignore_trace)] => derive_trace); + +fn derive_trace(mut s: Structure<'_>) -> proc_macro2::TokenStream { + s.filter(|bi| { + !bi.ast() + .attrs + .iter() + .any(|attr| attr.path.is_ident("unsafe_ignore_trace")) + }); + let trace_body = s.each(|bi| quote!(mark(#bi))); + + s.add_bounds(AddBounds::Fields); + let trace_impl = s.unsafe_bound_impl( + quote!(::boa_gc::Trace), + quote! { + #[inline] + unsafe fn trace(&self) { + #[allow(dead_code)] + #[inline] + fn mark(it: &T) { + unsafe { + ::boa_gc::Trace::trace(it); + } + } + match *self { #trace_body } + } + #[inline] + unsafe fn weak_trace(&self) { + #[allow(dead_code, unreachable_code)] + #[inline] + fn mark(it: &T) { + unsafe { + ::boa_gc::Trace::weak_trace(it) + } + } + match *self { #trace_body } + } + #[inline] + unsafe fn root(&self) { + #[allow(dead_code)] + #[inline] + fn mark(it: &T) { + unsafe { + ::boa_gc::Trace::root(it); + } + } + match *self { #trace_body } + } + #[inline] + unsafe fn unroot(&self) { + #[allow(dead_code)] + #[inline] + fn mark(it: &T) { + unsafe { + ::boa_gc::Trace::unroot(it); + } + } + match *self { #trace_body } + } + #[inline] + fn run_finalizer(&self) { + ::boa_gc::Finalize::finalize(self); + #[allow(dead_code)] + #[inline] + fn mark(it: &T) { + unsafe { + ::boa_gc::Trace::run_finalizer(it); + } + } + match *self { #trace_body } + } + }, + ); + + // We also implement drop to prevent unsafe drop implementations on this + // type and encourage people to use Finalize. This implementation will + // call `Finalize::finalize` if it is safe to do so. + let drop_impl = s.unbound_impl( + quote!(::std::ops::Drop), + quote! { + fn drop(&mut self) { + if ::boa_gc::finalizer_safe() { + ::boa_gc::Finalize::finalize(self); + } + } + }, + ); + + quote! { + #trace_impl + #drop_impl + } +} + +decl_derive!([Finalize] => derive_finalize); + +fn derive_finalize(s: Structure<'_>) -> proc_macro2::TokenStream { + s.unbound_impl(quote!(::boa_gc::Finalize), quote!()) +} diff --git a/boa_tester/Cargo.toml b/boa_tester/Cargo.toml index 2d55ff8aeeb..6f252beb774 100644 --- a/boa_tester/Cargo.toml +++ b/boa_tester/Cargo.toml @@ -25,6 +25,5 @@ regex = "1.7.0" once_cell = "1.16.0" colored = "2.0.0" fxhash = "0.2.1" -gc = { version = "0.4.1", features = ["derive"] } rayon = "1.5.3" anyhow = "1.0.66" diff --git a/boa_tester/src/exec/mod.rs b/boa_tester/src/exec/mod.rs index 4310c8219e0..dc948894646 100644 --- a/boa_tester/src/exec/mod.rs +++ b/boa_tester/src/exec/mod.rs @@ -12,7 +12,7 @@ use boa_engine::{ builtins::JsArgs, object::FunctionBuilder, property::Attribute, Context, JsNativeErrorKind, JsResult, JsValue, }; -use boa_gc::{Cell, Finalize, Gc, Trace}; +use boa_gc::{Finalize, Gc, GcCell, Trace}; use boa_parser::Parser; use colored::Colorize; use rayon::prelude::*; @@ -400,13 +400,13 @@ impl Test { /// Object which includes the result of the async operation. #[derive(Debug, Clone, Trace, Finalize)] struct AsyncResult { - inner: Gc>>, + inner: Gc>>, } impl Default for AsyncResult { fn default() -> Self { Self { - inner: Gc::new(Cell::new(Ok(()))), + inner: Gc::new(GcCell::new(Ok(()))), } } } From ff02cd07f1e7a2e5d27f8ce3848e765ba1773434 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Tue, 15 Nov 2022 17:21:52 +0000 Subject: [PATCH 10/11] Bump clap from 4.0.23 to 4.0.24 (#2439) Bumps [clap](https://github.com/clap-rs/clap) from 4.0.23 to 4.0.24.
Release notes

Sourced from clap's releases.

v4.0.24

[4.0.24] - 2022-11-14

Fixes

  • Avoid panic when printing an argument that isn't built
Changelog

Sourced from clap's changelog.

[4.0.24] - 2022-11-14

Fixes

  • Avoid panic when printing an argument that isn't built
Commits

[![Dependabot compatibility score](https://dependabot-badges.githubapp.com/badges/compatibility_score?dependency-name=clap&package-manager=cargo&previous-version=4.0.23&new-version=4.0.24)](https://docs.github.com/en/github/managing-security-vulnerabilities/about-dependabot-security-updates#about-compatibility-scores) Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`. [//]: # (dependabot-automerge-start) [//]: # (dependabot-automerge-end) ---
Dependabot commands and options
You can trigger Dependabot actions by commenting on this PR: - `@dependabot rebase` will rebase this PR - `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it - `@dependabot merge` will merge this PR after your CI passes on it - `@dependabot squash and merge` will squash and merge this PR after your CI passes on it - `@dependabot cancel merge` will cancel a previously requested merge and block automerging - `@dependabot reopen` will reopen this PR if it is closed - `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually - `@dependabot ignore this major version` will close this PR and stop Dependabot creating any more for this major version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this minor version` will close this PR and stop Dependabot creating any more for this minor version (unless you reopen the PR or upgrade to it yourself) - `@dependabot ignore this dependency` will close this PR and stop Dependabot creating any more for this dependency (unless you reopen the PR or upgrade to it yourself)
--- Cargo.lock | 8 ++++---- boa_cli/Cargo.toml | 2 +- boa_tester/Cargo.toml | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1447e5da805..dc216db0d53 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -85,7 +85,7 @@ dependencies = [ "boa_engine", "boa_interner", "boa_parser", - "clap 4.0.23", + "clap 4.0.24", "colored", "jemallocator", "phf", @@ -216,7 +216,7 @@ dependencies = [ "boa_gc", "boa_interner", "boa_parser", - "clap 4.0.23", + "clap 4.0.24", "colored", "fxhash", "once_cell", @@ -329,9 +329,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.0.23" +version = "4.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eb41c13df48950b20eb4cd0eefa618819469df1bffc49d11e8487c4ba0037e5" +checksum = "60494cedb60cb47462c0ff7be53de32c0e42a6fc2c772184554fa12bd9489c03" dependencies = [ "atty", "bitflags", diff --git a/boa_cli/Cargo.toml b/boa_cli/Cargo.toml index 312e4d05a82..659ff2b91b4 100644 --- a/boa_cli/Cargo.toml +++ b/boa_cli/Cargo.toml @@ -18,7 +18,7 @@ boa_interner.workspace = true boa_parser.workspace = true rustyline = "10.0.0" rustyline-derive = "0.7.0" -clap = { version = "4.0.23", features = ["derive"] } +clap = { version = "4.0.24", features = ["derive"] } serde_json = "1.0.87" colored = "2.0.0" regex = "1.7.0" diff --git a/boa_tester/Cargo.toml b/boa_tester/Cargo.toml index 6f252beb774..1119ac2ed35 100644 --- a/boa_tester/Cargo.toml +++ b/boa_tester/Cargo.toml @@ -16,7 +16,7 @@ boa_engine = { workspace = true, features = ["intl"] } boa_interner.workspace = true boa_gc.workspace = true boa_parser.workspace = true -clap = { version = "4.0.23", features = ["derive"] } +clap = { version = "4.0.24", features = ["derive"] } serde = { version = "1.0.147", features = ["derive"] } serde_yaml = "0.9.14" serde_json = "1.0.87" From c1b5f38d11615b1443e27112a1cba307261bfd9d Mon Sep 17 00:00:00 2001 From: Addison Crump Date: Tue, 15 Nov 2022 18:30:59 +0000 Subject: [PATCH 11/11] VM Fuzzer (#2401) This Pull Request offers a basic VM fuzzer which relies on implied oracles (namely, "does it crash or timeout?"). It changes the following: - Adds an insns_remaining field to Context, denoting the number of instructions remaining to execute (only available when fuzzing) - Adds a JsNativeError variant, denoting when the number of instructions has been exceeded (only available when fuzzing) - Adds a VM fuzzer which looks for cases where Boa may crash on an input This offers no guarantees about correctness, only assertion violations. Depends on #2400. Any issues I raise in association with this fuzzer will link back to this fuzzer. You may run the fuzzer using the following commands: ```bash $ cd boa_engine $ cargo +nightly fuzz run -s none vm-implied ``` Co-authored-by: Addison Crump --- boa_engine/src/context/mod.rs | 17 +++++++++++++ boa_engine/src/error.rs | 23 +++++++++++++++++ boa_engine/src/vm/mod.rs | 9 +++++++ fuzz/Cargo.lock | 31 +++++------------------ fuzz/Cargo.toml | 12 +++++++++ fuzz/README.md | 19 ++++++++++++++ fuzz/fuzz_targets/bytecompiler-implied.rs | 25 ++++++++++++++++++ fuzz/fuzz_targets/common.rs | 24 +++++++++++++++++- fuzz/fuzz_targets/vm-implied.rs | 19 ++++++++++++++ 9 files changed, 153 insertions(+), 26 deletions(-) create mode 100644 fuzz/fuzz_targets/bytecompiler-implied.rs create mode 100644 fuzz/fuzz_targets/vm-implied.rs diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index 47cc742b3d1..cc9691d4f58 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -97,6 +97,10 @@ pub struct Context { #[cfg(feature = "intl")] icu: icu::Icu, + /// Number of instructions remaining before a forced exit + #[cfg(feature = "fuzz")] + pub(crate) instructions_remaining: usize, + pub(crate) vm: Vm, pub(crate) promise_job_queue: VecDeque, @@ -593,6 +597,8 @@ pub struct ContextBuilder { interner: Option, #[cfg(feature = "intl")] icu: Option, + #[cfg(feature = "fuzz")] + instructions_remaining: usize, } impl ContextBuilder { @@ -615,6 +621,15 @@ impl ContextBuilder { Ok(self) } + /// Specifies the number of instructions remaining to the [`Context`]. + /// + /// This function is only available if the `fuzz` feature is enabled. + #[cfg(feature = "fuzz")] + pub fn instructions_remaining(mut self, instructions_remaining: usize) -> Self { + self.instructions_remaining = instructions_remaining; + self + } + /// Creates a new [`ContextBuilder`] with a default empty [`Interner`] /// and a default [`BoaProvider`] if the `intl` feature is enabled. pub fn new() -> Self { @@ -643,6 +658,8 @@ impl ContextBuilder { icu::Icu::new(Box::new(icu_testdata::get_provider())) .expect("Failed to initialize default icu data.") }), + #[cfg(feature = "fuzz")] + instructions_remaining: self.instructions_remaining, promise_job_queue: VecDeque::new(), }; diff --git a/boa_engine/src/error.rs b/boa_engine/src/error.rs index 00e124da31f..c76bee7f336 100644 --- a/boa_engine/src/error.rs +++ b/boa_engine/src/error.rs @@ -503,6 +503,17 @@ impl JsNativeError { Self::new(JsNativeErrorKind::Uri, Box::default(), None) } + /// Creates a new `JsNativeError` that indicates that the context hit its execution limit. This + /// is only used in a fuzzing context. + #[cfg(feature = "fuzz")] + pub fn no_instructions_remain() -> Self { + Self::new( + JsNativeErrorKind::NoInstructionsRemain, + Box::default(), + None, + ) + } + /// Sets the message of this error. /// /// # Examples @@ -619,6 +630,12 @@ impl JsNativeError { } JsNativeErrorKind::Type => (constructors.type_error().prototype(), ErrorKind::Type), JsNativeErrorKind::Uri => (constructors.uri_error().prototype(), ErrorKind::Uri), + #[cfg(feature = "fuzz")] + JsNativeErrorKind::NoInstructionsRemain => { + unreachable!( + "The NoInstructionsRemain native error cannot be converted to an opaque type." + ) + } }; let o = JsObject::from_proto_and_data(prototype, ObjectData::error(tag)); @@ -747,6 +764,10 @@ pub enum JsNativeErrorKind { /// [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 Uri, + /// Error thrown when no instructions remain. Only used in a fuzzing context; not a valid JS + /// error variant. + #[cfg(feature = "fuzz")] + NoInstructionsRemain, } impl std::fmt::Display for JsNativeErrorKind { @@ -760,6 +781,8 @@ impl std::fmt::Display for JsNativeErrorKind { JsNativeErrorKind::Syntax => "SyntaxError", JsNativeErrorKind::Type => "TypeError", JsNativeErrorKind::Uri => "UriError", + #[cfg(feature = "fuzz")] + JsNativeErrorKind::NoInstructionsRemain => "NoInstructionsRemain", } .fmt(f) } diff --git a/boa_engine/src/vm/mod.rs b/boa_engine/src/vm/mod.rs index 49ba10667e1..7f75db94aa2 100644 --- a/boa_engine/src/vm/mod.rs +++ b/boa_engine/src/vm/mod.rs @@ -7,6 +7,8 @@ use crate::{ vm::{call_frame::CatchAddresses, code_block::Readable}, Context, JsResult, JsValue, }; +#[cfg(feature = "fuzz")] +use crate::{JsError, JsNativeError}; use boa_interner::ToInternedString; use boa_profiler::Profiler; use std::{convert::TryInto, mem::size_of, time::Instant}; @@ -179,6 +181,13 @@ impl Context { }); while self.vm.frame().pc < self.vm.frame().code.code.len() { + #[cfg(feature = "fuzz")] + if self.instructions_remaining == 0 { + return Err(JsError::from_native(JsNativeError::no_instructions_remain())); + } else { + self.instructions_remaining -= 1; + } + let result = if self.vm.trace { let mut pc = self.vm.frame().pc; let opcode: Opcode = self diff --git a/fuzz/Cargo.lock b/fuzz/Cargo.lock index beb643d96dc..ab15b48ab56 100644 --- a/fuzz/Cargo.lock +++ b/fuzz/Cargo.lock @@ -59,7 +59,6 @@ dependencies = [ "chrono", "dyn-clone", "fast-float", - "gc", "indexmap", "num-bigint", "num-integer", @@ -93,7 +92,8 @@ dependencies = [ name = "boa_gc" version = "0.16.0" dependencies = [ - "gc", + "boa_macros", + "boa_profiler", ] [[package]] @@ -113,8 +113,10 @@ dependencies = [ name = "boa_macros" version = "0.16.0" dependencies = [ + "proc-macro2", "quote", "syn", + "synstructure", ] [[package]] @@ -167,9 +169,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" [[package]] name = "chrono" -version = "0.4.22" +version = "0.4.23" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfd4d1b31faaa3a89d7934dbded3111da0d2ef28e3ebccdb4f0179f5929d1ef1" +checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" dependencies = [ "iana-time-zone", "js-sys", @@ -263,27 +265,6 @@ version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "95765f67b4b18863968b4a1bd5bb576f732b29a4a28c7cd84c09fa3e2875f33c" -[[package]] -name = "gc" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3edaac0f5832202ebc99520cb77c932248010c4645d20be1dc62d6579f5b3752" -dependencies = [ - "gc_derive", -] - -[[package]] -name = "gc_derive" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60df8444f094ff7885631d80e78eb7d88c3c2361a98daaabb06256e4500db941" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - [[package]] name = "getrandom" version = "0.2.8" diff --git a/fuzz/Cargo.toml b/fuzz/Cargo.toml index cc86d751da9..bdd84c79cf4 100644 --- a/fuzz/Cargo.toml +++ b/fuzz/Cargo.toml @@ -27,3 +27,15 @@ name = "parser-idempotency" path = "fuzz_targets/parser-idempotency.rs" test = false doc = false + +[[bin]] +name = "vm-implied" +path = "fuzz_targets/vm-implied.rs" +test = false +doc = false + +[[bin]] +name = "bytecompiler-implied" +path = "fuzz_targets/bytecompiler-implied.rs" +test = false +doc = false diff --git a/fuzz/README.md b/fuzz/README.md index 097e46ccb97..9bb314bfd6b 100644 --- a/fuzz/README.md +++ b/fuzz/README.md @@ -35,3 +35,22 @@ following: information, as the inputs parsed between the two should be the same. In this way, this fuzzer can identify correctness issues present in the parser. + +## Bytecompiler Fuzzer + +The bytecompiler fuzzer, located in [bytecompiler-implied.rs](fuzz_targets/bytecompiler-implied.rs), identifies cases +which cause an assertion failure in the bytecompiler. These crashes can cause denial of service issues and may block the +discovery of crash cases in the VM fuzzer. + +## VM Fuzzer + +The VM fuzzer, located in [vm-implied.rs](fuzz_targets/vm-implied.rs), identifies crash cases in the VM. It does so by +generating an arbitrary AST, converting it to source code (to remove invalid inputs), then executing that source code. +Because we are not comparing against any invariants other than "does it crash", this fuzzer will only discover faults +which cause the VM to terminate unexpectedly, e.g. as a result of a panic. It will not discover logic errors present in +the VM. + +To ensure that the VM does not attempt to execute an infinite loop, Boa is restricted to a finite number of instructions +before the VM is terminated. If a program takes more than a second or so to execute, it likely indicates an issue in the +VM (as we expect the fuzzer to execute only a certain amount of instructions, which should take significantly less +time). diff --git a/fuzz/fuzz_targets/bytecompiler-implied.rs b/fuzz/fuzz_targets/bytecompiler-implied.rs new file mode 100644 index 00000000000..dd91bbc32a3 --- /dev/null +++ b/fuzz/fuzz_targets/bytecompiler-implied.rs @@ -0,0 +1,25 @@ +#![no_main] + +mod common; + +use crate::common::FuzzSource; +use boa_engine::Context; +use boa_parser::Parser; +use libfuzzer_sys::{fuzz_target, Corpus}; +use std::io::Cursor; + +fn do_fuzz(original: FuzzSource) -> Corpus { + let mut ctx = Context::builder() + .interner(original.interner) + .instructions_remaining(0) + .build(); + let mut parser = Parser::new(Cursor::new(&original.source)); + if let Ok(parsed) = parser.parse_all(ctx.interner_mut()) { + let _ = ctx.compile(&parsed); + Corpus::Keep + } else { + Corpus::Reject + } +} + +fuzz_target!(|original: FuzzSource| -> Corpus { do_fuzz(original) }); diff --git a/fuzz/fuzz_targets/common.rs b/fuzz/fuzz_targets/common.rs index 161a6145c00..8d4c50b5425 100644 --- a/fuzz/fuzz_targets/common.rs +++ b/fuzz/fuzz_targets/common.rs @@ -2,7 +2,7 @@ use boa_ast::{ visitor::{VisitWith, VisitorMut}, Expression, StatementList, }; -use boa_interner::{Interner, Sym}; +use boa_interner::{Interner, Sym, ToInternedString}; use libfuzzer_sys::arbitrary; use libfuzzer_sys::arbitrary::{Arbitrary, Unstructured}; use std::fmt::{Debug, Formatter}; @@ -72,3 +72,25 @@ impl Debug for FuzzData { .finish_non_exhaustive() } } + +pub struct FuzzSource { + pub interner: Interner, + pub source: String, +} + +impl<'a> Arbitrary<'a> for FuzzSource { + fn arbitrary(u: &mut Unstructured<'a>) -> arbitrary::Result { + let data = FuzzData::arbitrary(u)?; + let source = data.ast.to_interned_string(&data.interner); + Ok(Self { + interner: data.interner, + source, + }) + } +} + +impl Debug for FuzzSource { + fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result { + f.write_fmt(format_args!("Fuzzed source:\n{}", self.source)) + } +} diff --git a/fuzz/fuzz_targets/vm-implied.rs b/fuzz/fuzz_targets/vm-implied.rs new file mode 100644 index 00000000000..77d4c14b3b9 --- /dev/null +++ b/fuzz/fuzz_targets/vm-implied.rs @@ -0,0 +1,19 @@ +#![no_main] + +mod common; + +use crate::common::FuzzSource; +use boa_engine::{Context, JsResult, JsValue}; +use libfuzzer_sys::fuzz_target; + +fn do_fuzz(original: FuzzSource) -> JsResult { + let mut ctx = Context::builder() + .interner(original.interner) + .instructions_remaining(1 << 16) + .build(); + ctx.eval(&original.source) +} + +fuzz_target!(|original: FuzzSource| { + let _ = do_fuzz(original); +});