From 63d9d67ecb307be30b2ea53709e969c8b445eeb5 Mon Sep 17 00:00:00 2001 From: Iban Eguia Moraza Date: Mon, 24 Apr 2023 02:34:29 +0000 Subject: [PATCH] Added a Boa runtime (#2743) This Pull Request fixes/closes #718. It changes the following: - Adds a new `boa_runtime` crate, that will only include `console` for now - Changes the `boa_cli` crate to use the new `boa_runtime` crate for the console, instead of the `console` feature of `boa_engine` - Removes the `console` feature in `boa_engine` - Adds a new `boa_testing` helper crate with some useful functions for testing `boa`. This part duplicates the code from `boa_engine`, but I could not make `boa_engine` work with this crate as a dependency due to circular dependencies. Maybe doing it a bit generic could work, but didn't have enough time to check it. To be checked: wether the WASM example works as expected with the console. --- Cargo.lock | 83 +++--- Cargo.toml | 12 +- boa_ast/Cargo.toml | 2 +- boa_cli/Cargo.toml | 3 +- boa_cli/src/main.rs | 13 + boa_engine/Cargo.toml | 5 +- boa_engine/src/builtins/function/tests.rs | 3 +- boa_engine/src/builtins/mod.rs | 20 -- boa_engine/src/context/mod.rs | 17 +- boa_engine/src/lib.rs | 43 +-- boa_engine/src/value/display.rs | 90 +++--- boa_engine/src/value/tests.rs | 10 - boa_engine/src/vm/call_frame/mod.rs | 9 + boa_engine/src/vm/code_block.rs | 12 +- boa_examples/Cargo.toml | 3 +- boa_examples/src/bin/classes.rs | 19 +- boa_examples/src/bin/futures.rs | 22 +- boa_examples/src/bin/loadstring.rs | 2 +- boa_parser/Cargo.toml | 2 +- boa_runtime/Cargo.toml | 21 ++ .../src/console/mod.rs | 188 ++++++------ .../src/console/tests.rs | 24 +- boa_runtime/src/lib.rs | 277 ++++++++++++++++++ boa_tester/Cargo.toml | 2 +- boa_wasm/Cargo.toml | 2 +- test262 | 2 +- 26 files changed, 627 insertions(+), 259 deletions(-) create mode 100644 boa_runtime/Cargo.toml rename {boa_engine => boa_runtime}/src/console/mod.rs (85%) rename {boa_engine => boa_runtime}/src/console/tests.rs (77%) create mode 100644 boa_runtime/src/lib.rs diff --git a/Cargo.lock b/Cargo.lock index cffee4b071..0ee879bf60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -326,9 +326,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.2.0" +version = "2.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07d1372b52fa64b8af7cf6e3848cfa2dadb81c21e810f144bdd9c26e21895235" +checksum = "c70beb79cbb5ce9c4f8e20849978f34225931f665bb49efa6982875a4d5facb3" dependencies = [ "serde", ] @@ -362,7 +362,7 @@ name = "boa_ast" version = "0.16.0" dependencies = [ "arbitrary", - "bitflags 2.2.0", + "bitflags 2.1.0", "boa_interner", "boa_macros", "num-bigint", @@ -379,6 +379,7 @@ dependencies = [ "boa_gc", "boa_interner", "boa_parser", + "boa_runtime", "clap 4.2.4", "colored", "jemallocator", @@ -393,7 +394,7 @@ dependencies = [ name = "boa_engine" version = "0.16.0" dependencies = [ - "bitflags 2.2.0", + "bitflags 2.1.0", "boa_ast", "boa_gc", "boa_icu_provider", @@ -454,6 +455,7 @@ dependencies = [ "boa_gc", "boa_interner", "boa_parser", + "boa_runtime", "futures-util", "smol", ] @@ -521,7 +523,7 @@ dependencies = [ name = "boa_parser" version = "0.16.0" dependencies = [ - "bitflags 2.2.0", + "bitflags 2.1.0", "boa_ast", "boa_interner", "boa_macros", @@ -543,11 +545,22 @@ dependencies = [ "rustc-hash", ] +[[package]] +name = "boa_runtime" +version = "0.16.0" +dependencies = [ + "boa_engine", + "boa_gc", + "indoc", + "rustc-hash", + "textwrap", +] + [[package]] name = "boa_tester" version = "0.16.0" dependencies = [ - "bitflags 2.2.0", + "bitflags 2.1.0", "boa_engine", "boa_gc", "clap 4.2.4", @@ -585,9 +598,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.12.0" +version = "3.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d261e256854913907f67ed06efbc3338dfe6179796deefc1ff763fc1aee5535" +checksum = "9b1ce199063694f33ffb7dd4e0ee620741495c32833cde5aa08f02a0bf96f0c8" [[package]] name = "bytecheck" @@ -955,9 +968,9 @@ dependencies = [ [[package]] name = "cpufeatures" -version = "0.2.6" +version = "0.2.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280a9f2d8b3a38871a3c8a46fb80db65e5e5ed97da80c4d08bf27fb63e35e181" +checksum = "3e4c1eaa2012c47becbbad2ab175484c2a84d1185b566fb2cc5b8707343dfe58" dependencies = [ "libc", ] @@ -2031,9 +2044,9 @@ dependencies = [ [[package]] name = "icu_datetime" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a552f80b0f07c4cf45f9548e8b0b89ee4adb3be5fb2f49fb792cc178ed42be73" +checksum = "72d61014bb8604505baa84ed522aa039951bd81177828d165e80ea8a0543c8a7" dependencies = [ "databake", "displaydoc", @@ -2232,9 +2245,9 @@ dependencies = [ [[package]] name = "icu_provider_fs" -version = "1.2.0" +version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "741a8b57997e99ee8a20da1219b6dad8e4e243f3b05d2397b931f432b2797ebc" +checksum = "b861a88223338dcb70466b04918f1d8bc136575e6d252d1dc349ef4848d388e1" dependencies = [ "bincode", "crlify", @@ -2465,9 +2478,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.141" +version = "0.2.142" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3304a64d199bb964be99741b7a14d26972741915b3649639149b2479bb46f4b5" +checksum = "6a987beff54b60ffa6d51982e1aa1146bc42f19bd26be28b0586f252fccf5317" [[package]] name = "libloading" @@ -2496,9 +2509,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.3.1" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d59d8c75012853d2e872fb56bc8a2e53718e2cafe1a4c823143141c6d90c322f" +checksum = "36eb31c1778188ae1e64398743890d0877fef36d11521ac60406b42016e8c2cf" [[package]] name = "litemap" @@ -2560,9 +2573,9 @@ dependencies = [ [[package]] name = "matrixmultiply" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add85d4dd35074e6fedc608f8c8f513a3548619a9024b751949ef0e8e45a4d84" +checksum = "bb99c395ae250e1bf9133673f03ca9f97b7e71b705436bf8f089453445d1e9fe" dependencies = [ "rawpointer", ] @@ -2841,9 +2854,9 @@ checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" [[package]] name = "openssl" -version = "0.10.50" +version = "0.10.51" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e30d8bc91859781f0a943411186324d580f2bbeb71b452fe91ae344806af3f1" +checksum = "97ea2d98598bf9ada7ea6ee8a30fb74f9156b63bbe495d64ec2b87c269d2dda3" dependencies = [ "bitflags 1.3.2", "cfg-if 1.0.0", @@ -2873,9 +2886,9 @@ checksum = "ff011a302c396a5197692431fc1948019154afc178baf7d8e37367442a4601cf" [[package]] name = "openssl-sys" -version = "0.9.85" +version = "0.9.86" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d3d193fb1488ad46ffe3aaabc912cc931d02ee8518fe2959aea8ef52718b0c0" +checksum = "992bac49bdbab4423199c654a5515bd2a6c6a23bf03f2dd3bdb7e5ae6259bc69" dependencies = [ "cc", "libc", @@ -3077,9 +3090,9 @@ dependencies = [ [[package]] name = "polling" -version = "2.7.0" +version = "2.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4be1c66a6add46bff50935c313dae30a5030cf8385c5206e8a95e9e9def974aa" +checksum = "4b2d323e8ca7996b3e23126511a523f7e62924d93ecd5ae73b333815b0eb3dce" dependencies = [ "autocfg", "bitflags 1.3.2", @@ -3290,13 +3303,13 @@ dependencies = [ [[package]] name = "regex" -version = "1.8.0" +version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac6cf59af1067a3fb53fbe5c88c053764e930f932be1d71d3ffe032cbe147f59" +checksum = "af83e617f331cc6ae2da5443c602dfa5af81e517212d9d611a5b3ba1777b5370" dependencies = [ "aho-corasick", "memchr", - "regex-syntax 0.7.0", + "regex-syntax 0.7.1", ] [[package]] @@ -3317,9 +3330,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6868896879ba532248f33598de5181522d8b3d9d724dfd230911e1a7d4822f5" +checksum = "a5996294f19bd3aae0453a862ad728f60e6600695733dd5df01da90c54363a3c" [[package]] name = "region" @@ -3437,9 +3450,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.37.11" +version = "0.37.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85597d61f83914ddeba6a47b3b8ffe7365107221c2e557ed94426489fefb5f77" +checksum = "d9b864d3c18a5785a05953adeed93e2dca37ed30f18e69bba9f30079d51f363f" dependencies = [ "bitflags 1.3.2", "errno", @@ -4344,9 +4357,9 @@ dependencies = [ [[package]] name = "tracing-subscriber" -version = "0.3.16" +version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6176eae26dd70d0c919749377897b54a9276bd7061339665dd68777926b5a70" +checksum = "30a651bc37f915e81f087d86e62a18eec5f79550c7faff886f7090b4ea757c77" dependencies = [ "sharded-slab", "thread_local", diff --git a/Cargo.toml b/Cargo.toml index cad08fa89b..d47cb01a06 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "boa_macros_tests", "boa_parser", "boa_profiler", + "boa_runtime", "boa_tester", "boa_unicode", "boa_wasm", @@ -26,15 +27,16 @@ license = "Unlicense/MIT" description = "Boa is a Javascript lexer, parser and compiler written in Rust. Currently, it has support for some of the language." [workspace.dependencies] +boa_ast = { version = "0.16.0", path = "boa_ast" } boa_engine = { version = "0.16.0", path = "boa_engine" } -boa_interner = { version = "0.16.0", path = "boa_interner" } boa_gc = { version = "0.16.0", path = "boa_gc" } -boa_profiler = { version = "0.16.0", path = "boa_profiler" } -boa_unicode = { version = "0.16.0", path = "boa_unicode" } +boa_icu_provider = { version = "0.16.0", path = "boa_icu_provider" } +boa_interner = { version = "0.16.0", path = "boa_interner" } boa_macros = { version = "0.16.0", path = "boa_macros" } -boa_ast = { version = "0.16.0", path = "boa_ast" } boa_parser = { version = "0.16.0", path = "boa_parser" } -boa_icu_provider = { version = "0.16.0", path = "boa_icu_provider" } +boa_profiler = { version = "0.16.0", path = "boa_profiler" } +boa_runtime = { version = "0.16.0", path = "boa_runtime" } +boa_unicode = { version = "0.16.0", path = "boa_unicode" } [workspace.metadata.workspaces] allow_branch = "main" diff --git a/boa_ast/Cargo.toml b/boa_ast/Cargo.toml index 83ed602f36..192ad55fcf 100644 --- a/boa_ast/Cargo.toml +++ b/boa_ast/Cargo.toml @@ -18,7 +18,7 @@ arbitrary = ["dep:arbitrary", "boa_interner/arbitrary", "num-bigint/arbitrary"] boa_interner.workspace = true boa_macros.workspace = true rustc-hash = "1.1.0" -bitflags = "2.2.0" +bitflags = "2.1.0" num-bigint = "0.4.3" serde = { version = "1.0.160", features = ["derive"], optional = true } arbitrary = { version = "1", features = ["derive"], optional = true } diff --git a/boa_cli/Cargo.toml b/boa_cli/Cargo.toml index fdf3596fde..81b0cdf080 100644 --- a/boa_cli/Cargo.toml +++ b/boa_cli/Cargo.toml @@ -12,11 +12,12 @@ repository.workspace = true rust-version.workspace = true [dependencies] -boa_engine = { workspace = true, features = ["deser", "flowgraph", "trace", "console"] } +boa_engine = { workspace = true, features = ["deser", "flowgraph", "trace"] } boa_ast = { workspace = true, features = ["serde"] } boa_parser.workspace = true boa_gc.workspace = true boa_interner.workspace = true +boa_runtime.workspace = true rustyline = { version = "11.0.0", features = ["derive"]} clap = { version = "4.2.4", features = ["derive"] } serde_json = "1.0.96" diff --git a/boa_cli/src/main.rs b/boa_cli/src/main.rs index 427aa42431..af45a025bf 100644 --- a/boa_cli/src/main.rs +++ b/boa_cli/src/main.rs @@ -66,9 +66,11 @@ use boa_engine::{ context::ContextBuilder, job::{FutureJob, JobQueue, NativeJob}, optimizer::OptimizerOptions, + property::Attribute, vm::flowgraph::{Direction, Graph}, Context, JsResult, Source, }; +use boa_runtime::Console; use clap::{Parser, ValueEnum, ValueHint}; use colored::{Color, Colorize}; use debug::init_boa_debug_object; @@ -311,6 +313,9 @@ fn main() -> Result<(), io::Error> { // Strict mode context.strict(args.strict); + // Add `console`. + add_runtime(&mut context); + // Trace Output context.set_trace(args.trace); @@ -404,6 +409,14 @@ fn main() -> Result<(), io::Error> { Ok(()) } +/// Adds the CLI runtime to the context. +fn add_runtime(context: &mut Context<'_>) { + let console = Console::init(context); + context + .register_global_property(Console::NAME, console, Attribute::all()) + .expect("the console object shouldn't exist"); +} + #[derive(Default)] struct Jobs(RefCell>); diff --git a/boa_engine/Cargo.toml b/boa_engine/Cargo.toml index c6f13f0a14..93657825b7 100644 --- a/boa_engine/Cargo.toml +++ b/boa_engine/Cargo.toml @@ -42,9 +42,6 @@ flowgraph = [] # Enable Boa's VM instruction tracing. trace = [] -# Enable Boa's WHATWG console object implementation. -console = [] - # Enable Boa's additional ECMAScript features for web browsers. annex-b = ["boa_parser/annex-b"] @@ -64,7 +61,7 @@ regress = "0.5.0" rustc-hash = "1.1.0" num-bigint = { version = "0.4.3", features = ["serde"] } num-integer = "0.1.45" -bitflags = "2.2.0" +bitflags = "2.1.0" indexmap = "1.9.3" ryu-js = "0.2.2" chrono = { version = "0.4.24", default-features = false, features = ["clock", "std"] } diff --git a/boa_engine/src/builtins/function/tests.rs b/boa_engine/src/builtins/function/tests.rs index 9aece8f73f..ef77c4bb91 100644 --- a/boa_engine/src/builtins/function/tests.rs +++ b/boa_engine/src/builtins/function/tests.rs @@ -1,5 +1,3 @@ -use indoc::indoc; - use crate::{ builtins::error::ErrorKind, error::JsNativeError, @@ -9,6 +7,7 @@ use crate::{ property::{Attribute, PropertyDescriptor}, run_test_actions, JsValue, TestAction, }; +use indoc::indoc; #[allow(clippy::float_cmp)] #[test] diff --git a/boa_engine/src/builtins/mod.rs b/boa_engine/src/builtins/mod.rs index a6aa6e5f86..c6ad308cfa 100644 --- a/boa_engine/src/builtins/mod.rs +++ b/boa_engine/src/builtins/mod.rs @@ -1,6 +1,4 @@ //! Boa's ECMAScript built-in object implementations, e.g. Object, String, Math, Array, etc. -//! -//! This module also contains a JavaScript Console implementation. pub mod array; pub mod array_buffer; @@ -106,9 +104,6 @@ use crate::{ Context, JsResult, JsString, JsValue, }; -#[cfg(feature = "console")] -use crate::console::Console; - /// A [Well-Known Intrinsic Object]. /// /// Well-known intrinsics are built-in objects that are explicitly referenced by the algorithms of @@ -373,21 +368,6 @@ pub(crate) fn set_default_global_bindings(context: &mut Context<'_>) -> JsResult #[cfg(feature = "intl")] global_binding::(context)?; - #[cfg(feature = "console")] - { - let object = Console::init(context); - let global_object = context.global_object(); - global_object.define_property_or_throw( - utf16!("console"), - PropertyDescriptor::builder() - .value(object) - .writable(true) - .enumerable(true) - .configurable(true), - context, - )?; - } - Ok(()) } diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index 6554f56d40..a78c1b1dd8 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -300,6 +300,8 @@ impl<'host> Context<'host> { /// Register a global property. /// + /// It will return an error if the property is already defined. + /// /// # Example /// ``` /// use boa_engine::{ @@ -310,13 +312,17 @@ impl<'host> Context<'host> { /// /// let mut context = Context::default(); /// - /// context.register_global_property("myPrimitiveProperty", 10, Attribute::all()); + /// context + /// .register_global_property("myPrimitiveProperty", 10, Attribute::all()) + /// .expect("property shouldn't exist"); /// /// let object = ObjectInitializer::new(&mut context) /// .property("x", 0, Attribute::all()) /// .property("y", 1, Attribute::all()) /// .build(); - /// context.register_global_property("myObjectProperty", object, Attribute::all()); + /// context + /// .register_global_property("myObjectProperty", object, Attribute::all()) + /// .expect("property shouldn't exist"); /// ``` pub fn register_global_property( &mut self, @@ -411,6 +417,8 @@ impl<'host> Context<'host> { /// Register a global class of type `T`, where `T` implements `Class`. /// + /// It will return an error if the global property is already defined. + /// /// # Example /// ```ignore /// #[derive(Debug, Trace, Finalize)] @@ -515,6 +523,11 @@ impl<'host> Context<'host> { self.kept_alive.clear(); } + /// Retrieves the current stack trace of the context. + pub fn stack_trace(&mut self) -> impl Iterator { + self.vm.frames.iter().rev() + } + /// Replaces the currently active realm with `realm`, and returns the old realm. pub fn enter_realm(&mut self, realm: Realm) -> Realm { self.vm diff --git a/boa_engine/src/lib.rs b/boa_engine/src/lib.rs index 734337c643..5ba437c63b 100644 --- a/boa_engine/src/lib.rs +++ b/boa_engine/src/lib.rs @@ -42,7 +42,6 @@ //! # Crate Features //! //! - **serde** - Enables serialization and deserialization of the AST (Abstract Syntax Tree). -//! - **console** - Enables `boa`'s [WHATWG `console`][whatwg] object implementation. //! - **profiler** - Enables profiling with measureme (this is mostly internal). //! - **intl** - Enables `boa`'s [ECMA-402 Internationalization API][ecma-402] (`Intl` object) //! @@ -56,7 +55,6 @@ //! - **`boa_unicode`** - Boa's Unicode identifier. //! - **`boa_icu_provider`** - Boa's ICU4X data provider. //! -//! [whatwg]: https://console.spec.whatwg.org //! [ecma-402]: https://tc39.es/ecma402 //! [boa-conformance]: https://boajs.dev/boa/test262/ //! [boa-web]: https://boajs.dev/ @@ -156,15 +154,12 @@ pub mod property; pub mod realm; pub mod string; pub mod symbol; -pub mod value; -pub mod vm; - -#[cfg(feature = "console")] -pub mod console; - -pub(crate) mod tagged; +// pub(crate) mod tagged; +mod tagged; #[cfg(test)] mod tests; +pub mod value; +pub mod vm; /// A convenience module that re-exports the most commonly-used Boa APIs pub mod prelude { @@ -224,7 +219,7 @@ use std::borrow::Cow; /// A test action executed in a test function. #[cfg(test)] #[derive(Clone)] -pub(crate) struct TestAction(Inner); +struct TestAction(Inner); #[cfg(test)] #[derive(Clone)] @@ -264,12 +259,12 @@ enum Inner { #[cfg(test)] impl TestAction { /// Evaluates some utility functions used in tests. - pub(crate) const fn run_harness() -> Self { + const fn run_harness() -> Self { Self(Inner::RunHarness) } /// Runs `source`, panicking if the execution throws. - pub(crate) fn run(source: impl Into>) -> Self { + fn run(source: impl Into>) -> Self { Self(Inner::Run { source: source.into(), }) @@ -278,22 +273,19 @@ impl TestAction { /// Executes `op` with the currently active context. /// /// Useful to make custom assertions that must be done from Rust code. - pub(crate) fn inspect_context(op: fn(&mut Context<'_>)) -> Self { + fn inspect_context(op: fn(&mut Context<'_>)) -> Self { Self(Inner::InspectContext { op }) } /// Asserts that evaluating `source` returns the `true` value. - pub(crate) fn assert(source: impl Into>) -> Self { + fn assert(source: impl Into>) -> Self { Self(Inner::Assert { source: source.into(), }) } /// Asserts that the script returns `expected` when evaluating `source`. - pub(crate) fn assert_eq( - source: impl Into>, - expected: impl Into, - ) -> Self { + fn assert_eq(source: impl Into>, expected: impl Into) -> Self { Self(Inner::AssertEq { source: source.into(), expected: expected.into(), @@ -303,7 +295,7 @@ impl TestAction { /// Asserts that calling `op` with the value obtained from evaluating `source` returns `true`. /// /// Useful to check properties of the obtained value that cannot be checked from JS code. - pub(crate) fn assert_with_op( + fn assert_with_op( source: impl Into>, op: fn(JsValue, &mut Context<'_>) -> bool, ) -> Self { @@ -314,7 +306,7 @@ impl TestAction { } /// Asserts that evaluating `source` throws the opaque error `value`. - pub(crate) fn assert_opaque_error( + fn assert_opaque_error( source: impl Into>, value: impl Into, ) -> Self { @@ -325,7 +317,7 @@ impl TestAction { } /// Asserts that evaluating `source` throws a native error of `kind` and `message`. - pub(crate) fn assert_native_error( + fn assert_native_error( source: impl Into>, kind: builtins::error::ErrorKind, message: &'static str, @@ -338,7 +330,7 @@ impl TestAction { } /// Asserts that calling `op` with the currently executing context returns `true`. - pub(crate) fn assert_context(op: fn(&mut Context<'_>) -> bool) -> Self { + fn assert_context(op: fn(&mut Context<'_>) -> bool) -> Self { Self(Inner::AssertContext { op }) } } @@ -346,7 +338,7 @@ impl TestAction { /// Executes a list of test actions on a new, default context. #[cfg(test)] #[track_caller] -pub(crate) fn run_test_actions(actions: impl IntoIterator) { +fn run_test_actions(actions: impl IntoIterator) { let context = &mut Context::default(); run_test_actions_with(actions, context); } @@ -354,10 +346,7 @@ pub(crate) fn run_test_actions(actions: impl IntoIterator) { /// Executes a list of test actions on the provided context. #[cfg(test)] #[track_caller] -pub(crate) fn run_test_actions_with( - actions: impl IntoIterator, - context: &mut Context<'_>, -) { +fn run_test_actions_with(actions: impl IntoIterator, context: &mut Context<'_>) { #[track_caller] fn forward_val(context: &mut Context<'_>, source: &str) -> JsResult { context.eval_script(Source::from_bytes(source)) diff --git a/boa_engine/src/value/display.rs b/boa_engine/src/value/display.rs index ea54b0ffc1..7050198852 100644 --- a/boa_engine/src/value/display.rs +++ b/boa_engine/src/value/display.rs @@ -248,67 +248,69 @@ pub(crate) fn log_string_from(x: &JsValue, print_internals: bool, print_children } ) } - _ => display_obj(x, print_internals), + _ => x.display_obj(print_internals), } } _ => x.display().to_string(), } } -/// A helper function for specifically printing object values -pub(crate) fn display_obj(v: &JsValue, print_internals: bool) -> String { - // A simple helper for getting the address of a value - // TODO: Find a more general place for this, as it can be used in other situations as well - fn address_of(t: &T) -> usize { - let my_ptr: *const T = t; - my_ptr as usize - } +impl JsValue { + /// A helper function for specifically printing object values + pub fn display_obj(&self, print_internals: bool) -> String { + // A simple helper for getting the address of a value + // TODO: Find a more general place for this, as it can be used in other situations as well + fn address_of(t: &T) -> usize { + let my_ptr: *const T = t; + my_ptr as usize + } - fn display_obj_internal( - data: &JsValue, - encounters: &mut HashSet, - indent: usize, - print_internals: bool, - ) -> String { - if let JsValue::Object(ref v) = *data { - // The in-memory address of the current object - let addr = address_of(v.as_ref()); + fn display_obj_internal( + data: &JsValue, + encounters: &mut HashSet, + indent: usize, + print_internals: bool, + ) -> String { + if let JsValue::Object(ref v) = *data { + // The in-memory address of the current object + let addr = address_of(v.as_ref()); - // We need not continue if this object has already been - // printed up the current chain - if encounters.contains(&addr) { - return String::from("[Cycle]"); - } + // We need not continue if this object has already been + // printed up the current chain + if encounters.contains(&addr) { + return String::from("[Cycle]"); + } - // Mark the current object as encountered - encounters.insert(addr); + // Mark the current object as encountered + encounters.insert(addr); - let result = if print_internals { - print_obj_value!(all of v, display_obj_internal, indent, encounters).join(",\n") - } else { - print_obj_value!(props of v, display_obj_internal, indent, encounters, print_internals) + let result = if print_internals { + print_obj_value!(all of v, display_obj_internal, indent, encounters).join(",\n") + } else { + print_obj_value!(props of v, display_obj_internal, indent, encounters, print_internals) .join(",\n") - }; + }; - // If the current object is referenced in a different branch, - // it will not cause an infinite printing loop, so it is safe to be printed again - encounters.remove(&addr); + // If the current object is referenced in a different branch, + // it will not cause an infinite printing loop, so it is safe to be printed again + encounters.remove(&addr); - let closing_indent = String::from_utf8(vec![b' '; indent.wrapping_sub(4)]) - .expect("Could not create the closing brace's indentation string"); + let closing_indent = String::from_utf8(vec![b' '; indent.wrapping_sub(4)]) + .expect("Could not create the closing brace's indentation string"); - format!("{{\n{result}\n{closing_indent}}}") - } else { - // Every other type of data is printed with the display method - data.display().to_string() + format!("{{\n{result}\n{closing_indent}}}") + } else { + // Every other type of data is printed with the display method + data.display().to_string() + } } - } - // We keep track of which objects we have encountered by keeping their - // in-memory address in this set - let mut encounters = HashSet::new(); + // We keep track of which objects we have encountered by keeping their + // in-memory address in this set + let mut encounters = HashSet::new(); - display_obj_internal(v, &mut encounters, 4, print_internals) + display_obj_internal(self, &mut encounters, 4, print_internals) + } } impl Display for ValueDisplay<'_> { diff --git a/boa_engine/src/value/tests.rs b/boa_engine/src/value/tests.rs index 823edfc2ff..b714661efb 100644 --- a/boa_engine/src/value/tests.rs +++ b/boa_engine/src/value/tests.rs @@ -788,16 +788,6 @@ mod cyclic_conversions { 0.0, )]); } - - #[test] - fn console_log_cyclic() { - run_test_actions([TestAction::run(indoc! {r#" - let a = [1]; - a[1] = a; - console.log(a); - "#})]); - // Should not stack overflow - } } mod abstract_relational_comparison { diff --git a/boa_engine/src/vm/call_frame/mod.rs b/boa_engine/src/vm/call_frame/mod.rs index fb92fc5093..f8dc16d9fd 100644 --- a/boa_engine/src/vm/call_frame/mod.rs +++ b/boa_engine/src/vm/call_frame/mod.rs @@ -41,6 +41,15 @@ pub struct CallFrame { pub(crate) iterators: ThinVec<(JsObject, bool)>, } +/// ---- `CallFrame` public API ---- +impl CallFrame { + /// Retrieves the [`CodeBlock`] of this call frame. + #[inline] + pub const fn code_block(&self) -> &Gc { + &self.code_block + } +} + /// ---- `CallFrame` creation methods ---- impl CallFrame { /// Creates a new `CallFrame` with the provided `CodeBlock`. diff --git a/boa_engine/src/vm/code_block.rs b/boa_engine/src/vm/code_block.rs index 6c4b73112b..02d3247b9c 100644 --- a/boa_engine/src/vm/code_block.rs +++ b/boa_engine/src/vm/code_block.rs @@ -139,8 +139,9 @@ pub struct CodeBlock { pub(crate) trace: std::cell::Cell, } +/// ---- `CodeBlock` public API ---- impl CodeBlock { - /// Constructs a new `CodeBlock`. + /// Creates a new `CodeBlock`. #[must_use] pub fn new(name: Sym, length: u32, strict: bool) -> Self { Self { @@ -168,13 +169,22 @@ impl CodeBlock { } } + /// Retrieves the name associated with this code block. + #[must_use] + pub const fn name(&self) -> Sym { + self.name + } + /// Enable or disable instruction tracing to `stdout`. #[cfg(feature = "trace")] #[inline] pub fn set_trace(&self, value: bool) { self.trace.set(value); } +} +/// ---- `CodeBlock` private API ---- +impl CodeBlock { /// Read type T from code. /// /// # Safety diff --git a/boa_examples/Cargo.toml b/boa_examples/Cargo.toml index a30bc691d3..fde260b8c0 100644 --- a/boa_examples/Cargo.toml +++ b/boa_examples/Cargo.toml @@ -10,10 +10,11 @@ repository.workspace = true rust-version.workspace = true [dependencies] -boa_engine = { workspace = true, features = ["console"] } +boa_engine.workspace = true boa_ast.workspace = true boa_interner.workspace = true boa_gc.workspace = true boa_parser.workspace = true +boa_runtime.workspace = true smol = "1.3.0" futures-util = "0.3.28" diff --git a/boa_examples/src/bin/classes.rs b/boa_examples/src/bin/classes.rs index 3782ba87d9..54f6aeca8d 100644 --- a/boa_examples/src/bin/classes.rs +++ b/boa_examples/src/bin/classes.rs @@ -8,6 +8,7 @@ use boa_engine::{ }; use boa_gc::{Finalize, Trace}; +use boa_runtime::Console; // We create a new struct that is going to represent a person. // @@ -124,12 +125,26 @@ impl Class for Person { } } +/// Adds the custom runtime to the context. +fn add_runtime(context: &mut Context<'_>) { + // We first add the `console` object, to be able to call `console.log()`. + let console = Console::init(context); + context + .register_global_property(Console::NAME, console, Attribute::all()) + .expect("the console builtin shouldn't exist"); + + // Then we need to register the global class `Person` inside `context`. + context + .register_global_class::() + .expect("the Person builtin shouldn't exist"); +} + fn main() { // First we need to create a Javascript context. let mut context = Context::default(); - // Then we need to register the global class `Person` inside `context`. - context.register_global_class::().unwrap(); + // Then, we add our custom runtime. + add_runtime(&mut context); // Having done all of that, we can execute Javascript code with `eval`, // and access the `Person` class defined in Rust! diff --git a/boa_examples/src/bin/futures.rs b/boa_examples/src/bin/futures.rs index 8cb6432c0d..a7bee2ae46 100644 --- a/boa_examples/src/bin/futures.rs +++ b/boa_examples/src/bin/futures.rs @@ -8,8 +8,10 @@ use boa_engine::{ context::ContextBuilder, job::{FutureJob, JobQueue, NativeJob}, native_function::NativeFunction, + property::Attribute, Context, JsArgs, JsResult, JsValue, Source, }; +use boa_runtime::Console; use futures_util::{stream::FuturesUnordered, Future}; use smol::{future, stream::StreamExt, LocalExecutor}; @@ -128,16 +130,28 @@ fn delay( } } +/// Adds the custom runtime to the context. +fn add_runtime(context: &mut Context<'_>) { + // First add the `console` object, to be able to call `console.log()`. + let console = Console::init(context); + context + .register_global_property(Console::NAME, console, Attribute::all()) + .expect("the console builtin shouldn't exist"); + + // Then, bind the defined async function to the ECMAScript function "delay". + context + .register_global_builtin_callable("delay", 1, NativeFunction::from_async_fn(delay)) + .expect("the delay builtin shouldn't exist"); +} + fn main() { // Initialize the required executors and the context let executor = LocalExecutor::new(); let queue: &dyn JobQueue = &Queue::new(executor); let context = &mut ContextBuilder::new().job_queue(queue).build().unwrap(); - // Bind the defined async function to the ECMAScript function "delay". - context - .register_global_builtin_callable("delay", 1, NativeFunction::from_async_fn(delay)) - .unwrap(); + // Then, add a custom runtime. + add_runtime(context); // Multiple calls to multiple async timers. let script = r#" diff --git a/boa_examples/src/bin/loadstring.rs b/boa_examples/src/bin/loadstring.rs index cd394df6f8..52fc116a6e 100644 --- a/boa_examples/src/bin/loadstring.rs +++ b/boa_examples/src/bin/loadstring.rs @@ -3,7 +3,7 @@ use boa_engine::{Context, Source}; fn main() { - let js_code = "console.log('Hello World from a JS code string!')"; + let js_code = "'Hello World ' + 'from a JS code ' + 'string!'"; // Instantiate the execution context let mut context = Context::default(); diff --git a/boa_parser/Cargo.toml b/boa_parser/Cargo.toml index b06eba4fe7..d7592e3cc7 100644 --- a/boa_parser/Cargo.toml +++ b/boa_parser/Cargo.toml @@ -19,7 +19,7 @@ boa_unicode.workspace = true rustc-hash = "1.1.0" fast-float = "0.2.0" num-traits = "0.2.15" -bitflags = "2.2.0" +bitflags = "2.1.0" num-bigint = "0.4.3" regress = "0.5.0" diff --git a/boa_runtime/Cargo.toml b/boa_runtime/Cargo.toml new file mode 100644 index 0000000000..d7ab96c0f1 --- /dev/null +++ b/boa_runtime/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "boa_runtime" +description = "Example runtime for the Boa JavaScript engine." +keywords = ["javascript", "js", "runtime"] +categories = ["command-line-utilities"] +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + + +[dependencies] +boa_engine.workspace = true +boa_gc.workspace = true +rustc-hash = "1.1.0" + +[dev-dependencies] +indoc = "2.0.1" +textwrap = "0.16.0" diff --git a/boa_engine/src/console/mod.rs b/boa_runtime/src/console/mod.rs similarity index 85% rename from boa_engine/src/console/mod.rs rename to boa_runtime/src/console/mod.rs index 62db4a88fd..a699506e1a 100644 --- a/boa_engine/src/console/mod.rs +++ b/boa_runtime/src/console/mod.rs @@ -16,14 +16,14 @@ #[cfg(test)] mod tests; -use crate::{ +use boa_engine::{ native_function::NativeFunction, object::{JsObject, ObjectInitializer}, - value::{display::display_obj, JsValue, Numeric}, + value::{JsValue, Numeric}, Context, JsArgs, JsResult, JsString, }; use boa_gc::{Finalize, Trace}; -use boa_profiler::Profiler; +// use boa_profiler::Profiler; use rustc_hash::FxHashMap; use std::{cell::RefCell, rc::Rc, time::SystemTime}; @@ -51,7 +51,7 @@ fn logger(msg: LogMessage, console_state: &Console) { } /// This represents the `console` formatter. -pub fn formatter(data: &[JsValue], context: &mut Context<'_>) -> JsResult { +fn formatter(data: &[JsValue], context: &mut Context<'_>) -> JsResult { match data { [] => Ok(String::new()), [val] => Ok(val.to_string(context)?.to_std_string_escaped()), @@ -124,16 +124,18 @@ pub fn formatter(data: &[JsValue], context: &mut Context<'_>) -> JsResult, timer_map: FxHashMap, groups: Vec, } impl Console { - const NAME: &'static str = "console"; + /// Name of the built-in `console` property. + pub const NAME: &'static str = "console"; - pub(crate) fn init(context: &mut Context<'_>) -> JsObject { + /// Initializes the `console` built-in object. + pub fn init(context: &mut Context<'_>) -> JsObject { fn console_method( f: fn(&JsValue, &[JsValue], &Console, &mut Context<'_>) -> JsResult, state: Rc>, @@ -156,11 +158,11 @@ impl Console { }) } } - let _timer = Profiler::global().start_event(Self::NAME, "init"); + // let _timer = Profiler::global().start_event(Self::NAME, "init"); - let state = Rc::new(RefCell::new(Console::default())); + let state = Rc::new(RefCell::new(Self::default())); - ObjectInitializer::with_native(Console::default(), context) + ObjectInitializer::with_native(Self::default(), context) .function(console_method(Self::assert, state.clone()), "assert", 0) .function(console_method_mut(Self::clear, state.clone()), "clear", 0) .function(console_method(Self::debug, state.clone()), "debug", 0) @@ -209,10 +211,10 @@ impl Console { /// /// [spec]: https://console.spec.whatwg.org/#assert /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/assert - pub(crate) fn assert( + fn assert( _: &JsValue, args: &[JsValue], - console: &Console, + console: &Self, context: &mut Context<'_>, ) -> JsResult { let assertion = args.get(0).map_or(false, JsValue::to_boolean); @@ -246,10 +248,10 @@ impl Console { /// [spec]: https://console.spec.whatwg.org/#clear /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/clear #[allow(clippy::unnecessary_wraps)] - pub(crate) fn clear( + fn clear( _: &JsValue, _: &[JsValue], - console: &mut Console, + console: &mut Self, _: &mut Context<'_>, ) -> JsResult { console.groups.clear(); @@ -266,10 +268,10 @@ impl Console { /// /// [spec]: https://console.spec.whatwg.org/#debug /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/debug - pub(crate) fn debug( + fn debug( _: &JsValue, args: &[JsValue], - console: &Console, + console: &Self, context: &mut Context<'_>, ) -> JsResult { logger(LogMessage::Log(formatter(args, context)?), console); @@ -286,10 +288,10 @@ impl Console { /// /// [spec]: https://console.spec.whatwg.org/#error /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/error - pub(crate) fn error( + fn error( _: &JsValue, args: &[JsValue], - console: &Console, + console: &Self, context: &mut Context<'_>, ) -> JsResult { logger(LogMessage::Error(formatter(args, context)?), console); @@ -306,10 +308,10 @@ impl Console { /// /// [spec]: https://console.spec.whatwg.org/#info /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/info - pub(crate) fn info( + fn info( _: &JsValue, args: &[JsValue], - console: &Console, + console: &Self, context: &mut Context<'_>, ) -> JsResult { logger(LogMessage::Info(formatter(args, context)?), console); @@ -326,31 +328,16 @@ impl Console { /// /// [spec]: https://console.spec.whatwg.org/#log /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/log - pub(crate) fn log( + fn log( _: &JsValue, args: &[JsValue], - console: &Console, + console: &Self, context: &mut Context<'_>, ) -> JsResult { logger(LogMessage::Log(formatter(args, context)?), console); Ok(JsValue::undefined()) } - fn get_stack_trace(context: &mut Context<'_>) -> Vec { - let mut stack_trace: Vec = vec![]; - - for frame in context.vm.frames.iter().rev() { - stack_trace.push( - context - .interner() - .resolve_expect(frame.code_block.name) - .to_string(), - ); - } - - stack_trace - } - /// `console.trace(...data)` /// /// Prints a stack trace with "trace" logLevel, optionally labelled by data. @@ -361,16 +348,23 @@ impl Console { /// /// [spec]: https://console.spec.whatwg.org/#trace /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/trace - pub(crate) fn trace( + fn trace( _: &JsValue, args: &[JsValue], - console: &Console, + console: &Self, context: &mut Context<'_>, ) -> JsResult { if !args.is_empty() { logger(LogMessage::Log(formatter(args, context)?), console); - let stack_trace_dump = Self::get_stack_trace(context).join("\n"); + let stack_trace_dump = context + .stack_trace() + .map(|frame| frame.code_block().name()) + .collect::>() + .into_iter() + .map(|s| context.interner().resolve_expect(s).to_string()) + .collect::>() + .join("\n"); logger(LogMessage::Log(stack_trace_dump), console); } @@ -387,10 +381,10 @@ impl Console { /// /// [spec]: https://console.spec.whatwg.org/#warn /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/warn - pub(crate) fn warn( + fn warn( _: &JsValue, args: &[JsValue], - console: &Console, + console: &Self, context: &mut Context<'_>, ) -> JsResult { logger(LogMessage::Warn(formatter(args, context)?), console); @@ -407,10 +401,10 @@ impl Console { /// /// [spec]: https://console.spec.whatwg.org/#count /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/count - pub(crate) fn count( + fn count( _: &JsValue, args: &[JsValue], - console: &mut Console, + console: &mut Self, context: &mut Context<'_>, ) -> JsResult { let label = match args.get(0) { @@ -436,10 +430,10 @@ impl Console { /// /// [spec]: https://console.spec.whatwg.org/#countreset /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/countReset - pub(crate) fn count_reset( + fn count_reset( _: &JsValue, args: &[JsValue], - console: &mut Console, + console: &mut Self, context: &mut Context<'_>, ) -> JsResult { let label = match args.get(0) { @@ -475,10 +469,10 @@ impl Console { /// /// [spec]: https://console.spec.whatwg.org/#time /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/time - pub(crate) fn time( + fn time( _: &JsValue, args: &[JsValue], - console: &mut Console, + console: &mut Self, context: &mut Context<'_>, ) -> JsResult { let label = match args.get(0) { @@ -512,10 +506,10 @@ impl Console { /// /// [spec]: https://console.spec.whatwg.org/#timelog /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/timeLog - pub(crate) fn time_log( + fn time_log( _: &JsValue, args: &[JsValue], - console: &Console, + console: &Self, context: &mut Context<'_>, ) -> JsResult { let label = match args.get(0) { @@ -523,22 +517,25 @@ impl Console { None => "default".into(), }; - if let Some(t) = console.timer_map.get(&label) { - let time = Self::system_time_in_ms(); - let mut concat = format!("{}: {} ms", label.to_std_string_escaped(), time - t); - for msg in args.iter().skip(1) { - concat = concat + " " + &msg.display().to_string(); - } - logger(LogMessage::Log(concat), console); - } else { - logger( - LogMessage::Warn(format!( - "Timer '{}' doesn't exist", - label.to_std_string_escaped() - )), - console, - ); - } + console.timer_map.get(&label).map_or_else( + || { + logger( + LogMessage::Warn(format!( + "Timer '{}' doesn't exist", + label.to_std_string_escaped() + )), + console, + ); + }, + |t| { + let time = Self::system_time_in_ms(); + let mut concat = format!("{}: {} ms", label.to_std_string_escaped(), time - t); + for msg in args.iter().skip(1) { + concat = concat + " " + &msg.display().to_string(); + } + logger(LogMessage::Log(concat), console); + }, + ); Ok(JsValue::undefined()) } @@ -553,10 +550,10 @@ impl Console { /// /// [spec]: https://console.spec.whatwg.org/#timeend /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/timeEnd - pub(crate) fn time_end( + fn time_end( _: &JsValue, args: &[JsValue], - console: &mut Console, + console: &mut Self, context: &mut Context<'_>, ) -> JsResult { let label = match args.get(0) { @@ -564,25 +561,28 @@ impl Console { None => "default".into(), }; - if let Some(t) = console.timer_map.remove(&label) { - let time = Self::system_time_in_ms(); - logger( - LogMessage::Info(format!( - "{}: {} ms - timer removed", - label.to_std_string_escaped(), - time - t - )), - console, - ); - } else { - logger( - LogMessage::Warn(format!( - "Timer '{}' doesn't exist", - label.to_std_string_escaped() - )), - console, - ); - } + console.timer_map.remove(&label).map_or_else( + || { + logger( + LogMessage::Warn(format!( + "Timer '{}' doesn't exist", + label.to_std_string_escaped() + )), + console, + ); + }, + |t| { + let time = Self::system_time_in_ms(); + logger( + LogMessage::Info(format!( + "{}: {} ms - timer removed", + label.to_std_string_escaped(), + time - t + )), + console, + ); + }, + ); Ok(JsValue::undefined()) } @@ -597,10 +597,10 @@ impl Console { /// /// [spec]: https://console.spec.whatwg.org/#group /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/group - pub(crate) fn group( + fn group( _: &JsValue, args: &[JsValue], - console: &mut Console, + console: &mut Self, context: &mut Context<'_>, ) -> JsResult { let group_label = formatter(args, context)?; @@ -622,10 +622,10 @@ impl Console { /// [spec]: https://console.spec.whatwg.org/#groupend /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/groupEnd #[allow(clippy::unnecessary_wraps)] - pub(crate) fn group_end( + fn group_end( _: &JsValue, _: &[JsValue], - console: &mut Console, + console: &mut Self, _: &mut Context<'_>, ) -> JsResult { console.groups.pop(); @@ -644,14 +644,14 @@ impl Console { /// [spec]: https://console.spec.whatwg.org/#dir /// [mdn]: https://developer.mozilla.org/en-US/docs/Web/API/console/dir #[allow(clippy::unnecessary_wraps)] - pub(crate) fn dir( + fn dir( _: &JsValue, args: &[JsValue], - console: &Console, + console: &Self, _: &mut Context<'_>, ) -> JsResult { logger( - LogMessage::Info(display_obj(args.get_or_undefined(0), true)), + LogMessage::Info(args.get_or_undefined(0).display_obj(true)), console, ); Ok(JsValue::undefined()) diff --git a/boa_engine/src/console/tests.rs b/boa_runtime/src/console/tests.rs similarity index 77% rename from boa_engine/src/console/tests.rs rename to boa_runtime/src/console/tests.rs index d490e8de58..1b03025671 100644 --- a/boa_engine/src/console/tests.rs +++ b/boa_runtime/src/console/tests.rs @@ -1,4 +1,7 @@ -use crate::{console::formatter, run_test_actions, JsValue, TestAction}; +use super::{formatter, Console}; +use crate::test::{run_test_actions, run_test_actions_with, TestAction}; +use boa_engine::{property::Attribute, Context, JsValue}; +use indoc::indoc; #[test] fn formatter_no_args_is_empty_string() { @@ -81,3 +84,22 @@ fn formatter_float_format_works() { ); })]); } + +#[test] +fn console_log_cyclic() { + let mut context = Context::default(); + let console = Console::init(&mut context); + context + .register_global_property(Console::NAME, console, Attribute::all()) + .unwrap(); + + run_test_actions_with( + [TestAction::run(indoc! {r#" + let a = [1]; + a[1] = a; + console.log(a); + "#})], + &mut context, + ); + // Should not stack overflow +} diff --git a/boa_runtime/src/lib.rs b/boa_runtime/src/lib.rs new file mode 100644 index 0000000000..965025d997 --- /dev/null +++ b/boa_runtime/src/lib.rs @@ -0,0 +1,277 @@ +//! Example runtime for Boa +//! +//! This crate contains an example runtime for the `boa_engine` crate, so that it can be used as a +//! template for runtime implementors. It contains some basic functionality that can be used by +//! other crates. + +#![doc( + html_logo_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg", + html_favicon_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg" +)] +#![cfg_attr(not(test), forbid(clippy::unwrap_used))] +#![warn(missing_docs, clippy::dbg_macro)] +#![deny( + // rustc lint groups https://doc.rust-lang.org/rustc/lints/groups.html + warnings, + future_incompatible, + let_underscore, + nonstandard_style, + rust_2018_compatibility, + rust_2018_idioms, + rust_2021_compatibility, + unused, + + // rustc allowed-by-default lints https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html + macro_use_extern_crate, + meta_variable_misuse, + missing_abi, + missing_copy_implementations, + missing_debug_implementations, + non_ascii_idents, + noop_method_call, + single_use_lifetimes, + trivial_casts, + trivial_numeric_casts, + unreachable_pub, + unsafe_op_in_unsafe_fn, + unused_crate_dependencies, + unused_import_braces, + unused_lifetimes, + unused_qualifications, + unused_tuple_struct_fields, + variant_size_differences, + + // rustdoc lints https://doc.rust-lang.org/rustdoc/lints.html + rustdoc::broken_intra_doc_links, + rustdoc::private_intra_doc_links, + rustdoc::missing_crate_level_docs, + rustdoc::private_doc_tests, + rustdoc::invalid_codeblock_attributes, + rustdoc::invalid_rust_codeblocks, + rustdoc::bare_urls, + + // clippy categories https://doc.rust-lang.org/clippy/ + clippy::all, + clippy::correctness, + clippy::suspicious, + clippy::style, + clippy::complexity, + clippy::perf, + clippy::pedantic, + clippy::nursery, + clippy::undocumented_unsafe_blocks +)] +#![allow( + clippy::module_name_repetitions, + clippy::redundant_pub_crate, + clippy::let_unit_value +)] + +mod console; + +#[doc(inline)] +pub use console::Console; + +#[cfg(test)] +pub(crate) mod test { + use boa_engine::{builtins, Context, JsResult, JsValue, Source}; + use std::borrow::Cow; + + /// A test action executed in a test function. + #[allow(missing_debug_implementations)] + #[derive(Clone)] + pub(crate) struct TestAction(Inner); + + #[derive(Clone)] + #[allow(dead_code)] + enum Inner { + RunHarness, + Run { + source: Cow<'static, str>, + }, + InspectContext { + op: fn(&mut Context<'_>), + }, + Assert { + source: Cow<'static, str>, + }, + AssertEq { + source: Cow<'static, str>, + expected: JsValue, + }, + AssertWithOp { + source: Cow<'static, str>, + op: fn(JsValue, &mut Context<'_>) -> bool, + }, + AssertOpaqueError { + source: Cow<'static, str>, + expected: JsValue, + }, + AssertNativeError { + source: Cow<'static, str>, + kind: builtins::error::ErrorKind, + message: &'static str, + }, + AssertContext { + op: fn(&mut Context<'_>) -> bool, + }, + } + + impl TestAction { + /// Runs `source`, panicking if the execution throws. + pub(crate) fn run(source: impl Into>) -> Self { + Self(Inner::Run { + source: source.into(), + }) + } + + /// Executes `op` with the currently active context. + /// + /// Useful to make custom assertions that must be done from Rust code. + pub(crate) fn inspect_context(op: fn(&mut Context<'_>)) -> Self { + Self(Inner::InspectContext { op }) + } + } + + /// Executes a list of test actions on a new, default context. + #[track_caller] + pub(crate) fn run_test_actions(actions: impl IntoIterator) { + let context = &mut Context::default(); + run_test_actions_with(actions, context); + } + + /// Executes a list of test actions on the provided context. + #[track_caller] + #[allow(clippy::too_many_lines, clippy::missing_panics_doc)] + pub(crate) fn run_test_actions_with( + actions: impl IntoIterator, + context: &mut Context<'_>, + ) { + #[track_caller] + fn forward_val(context: &mut Context<'_>, source: &str) -> JsResult { + context.eval_script(Source::from_bytes(source)) + } + + #[track_caller] + fn fmt_test(source: &str, test: usize) -> String { + format!( + "\n\nTest case {test}: \n```\n{}\n```", + textwrap::indent(source, " ") + ) + } + + // Some unwrapping patterns look weird because they're replaceable + // by simpler patterns like `unwrap_or_else` or `unwrap_err + let mut i = 1; + for action in actions.into_iter().map(|a| a.0) { + match action { + Inner::RunHarness => { + // add utility functions for testing + // TODO: extract to a file + forward_val( + context, + r#" + function equals(a, b) { + if (Array.isArray(a) && Array.isArray(b)) { + return arrayEquals(a, b); + } + return a === b; + } + function arrayEquals(a, b) { + return Array.isArray(a) && + Array.isArray(b) && + a.length === b.length && + a.every((val, index) => equals(val, b[index])); + } + "#, + ) + .expect("failed to evaluate test harness"); + } + Inner::Run { source } => { + if let Err(e) = forward_val(context, &source) { + panic!("{}\nUncaught {e}", fmt_test(&source, i)); + } + } + Inner::InspectContext { op } => { + op(context); + } + Inner::Assert { source } => { + let val = match forward_val(context, &source) { + Err(e) => panic!("{}\nUncaught {e}", fmt_test(&source, i)), + Ok(v) => v, + }; + let Some(val) = val.as_boolean() else { + panic!( + "{}\nTried to assert with the non-boolean value `{}`", + fmt_test(&source, i), + val.display() + ) + }; + assert!(val, "{}", fmt_test(&source, i)); + i += 1; + } + Inner::AssertEq { source, expected } => { + let val = match forward_val(context, &source) { + Err(e) => panic!("{}\nUncaught {e}", fmt_test(&source, i)), + Ok(v) => v, + }; + assert_eq!(val, expected, "{}", fmt_test(&source, i)); + i += 1; + } + Inner::AssertWithOp { source, op } => { + let val = match forward_val(context, &source) { + Err(e) => panic!("{}\nUncaught {e}", fmt_test(&source, i)), + Ok(v) => v, + }; + assert!(op(val, context), "{}", fmt_test(&source, i)); + i += 1; + } + Inner::AssertOpaqueError { source, expected } => { + let err = match forward_val(context, &source) { + Ok(v) => panic!( + "{}\nExpected error, got value `{}`", + fmt_test(&source, i), + v.display() + ), + Err(e) => e, + }; + let Some(err) = err.as_opaque() else { + panic!("{}\nExpected opaque error, got native error `{}`", fmt_test(&source, i), err) + }; + + assert_eq!(err, &expected, "{}", fmt_test(&source, i)); + i += 1; + } + Inner::AssertNativeError { + source, + kind, + message, + } => { + let err = match forward_val(context, &source) { + Ok(v) => panic!( + "{}\nExpected error, got value `{}`", + fmt_test(&source, i), + v.display() + ), + Err(e) => e, + }; + let native = match err.try_native(context) { + Ok(err) => err, + Err(e) => panic!( + "{}\nCouldn't obtain a native error: {e}", + fmt_test(&source, i) + ), + }; + + assert_eq!(&native.kind, &kind, "{}", fmt_test(&source, i)); + assert_eq!(native.message(), message, "{}", fmt_test(&source, i)); + i += 1; + } + Inner::AssertContext { op } => { + assert!(op(context), "Test case {i}"); + i += 1; + } + } + } + } +} diff --git a/boa_tester/Cargo.toml b/boa_tester/Cargo.toml index 7085987339..50d79e47cc 100644 --- a/boa_tester/Cargo.toml +++ b/boa_tester/Cargo.toml @@ -18,7 +18,7 @@ clap = { version = "4.2.4", features = ["derive"] } serde = { version = "1.0.160", features = ["derive"] } serde_yaml = "0.9.21" serde_json = "1.0.96" -bitflags = "2.2.0" +bitflags = "2.1.0" regex = "1.8.0" once_cell = "1.17.1" colored = "2.0.0" diff --git a/boa_wasm/Cargo.toml b/boa_wasm/Cargo.toml index 315642c63f..94a6f6b19a 100644 --- a/boa_wasm/Cargo.toml +++ b/boa_wasm/Cargo.toml @@ -12,7 +12,7 @@ repository.workspace = true rust-version.workspace = true [dependencies] -boa_engine = { workspace = true, features = ["console"] } +boa_engine.workspace = true wasm-bindgen = "0.2.84" getrandom = { version = "0.2.9", features = ["js"] } chrono = { version = "0.4.24", features = ["clock", "std", "wasmbind"] } diff --git a/test262 b/test262 index e5a7450095..2df6c7d29a 160000 --- a/test262 +++ b/test262 @@ -1 +1 @@ -Subproject commit e5a7450095c52681cd6233035aba7d6f55b6f9af +Subproject commit 2df6c7d29a18cd33120e791bdbe2043980a893fd