From 515d28f0a2d17db00aa0f41a32de46b2e28c5ff9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Juli=C3=A1n=20Espina?= Date: Tue, 31 Jan 2023 04:54:24 +0000 Subject: [PATCH] Create `Source` to abstract JS code sources (#2579) Slightly related to #2411 since we need an API to pass module files, but more useful for #1760, #1313 and other error reporting issues. It changes the following: - Introduces a new `Source` API to store the path of a provided file or `None` if the source is a plain string. - Improves the display of `boa_tester` to show the path of the tests being run. This also enables hyperlinks to directly jump to the tested file from the VS terminal. - Adjusts the repo to this change. Hopefully, this will improve our error display in the future. --- Cargo.lock | 1 - boa_cli/src/main.rs | 21 ++--- boa_engine/benches/full.rs | 8 +- boa_engine/src/builtins/array/tests.rs | 109 ++++++++++++++++------ boa_engine/src/builtins/eval/mod.rs | 4 +- boa_engine/src/builtins/function/mod.rs | 25 ++--- boa_engine/src/builtins/json/mod.rs | 4 +- boa_engine/src/builtins/object/tests.rs | 12 ++- boa_engine/src/builtins/promise/tests.rs | 4 +- boa_engine/src/builtins/weak/weak_ref.rs | 10 +- boa_engine/src/context/hooks.rs | 4 +- boa_engine/src/context/mod.rs | 47 ++++++---- boa_engine/src/lib.rs | 23 +++-- boa_engine/src/tests.rs | 6 +- boa_engine/src/value/serde_json.rs | 26 +++--- boa_engine/src/vm/tests.rs | 18 ++-- boa_examples/src/bin/classes.rs | 6 +- boa_examples/src/bin/closures.rs | 12 +-- boa_examples/src/bin/commuter_visitor.rs | 9 +- boa_examples/src/bin/loadfile.rs | 6 +- boa_examples/src/bin/loadstring.rs | 4 +- boa_examples/src/bin/modulehandler.rs | 6 +- boa_examples/src/bin/symbol_visitor.rs | 9 +- boa_parser/src/lib.rs | 2 + boa_parser/src/parser/mod.rs | 71 ++++++-------- boa_parser/src/parser/tests/format/mod.rs | 5 +- boa_parser/src/parser/tests/mod.rs | 6 +- boa_parser/src/source.rs | 88 +++++++++++++++++ boa_tester/Cargo.toml | 1 - boa_tester/src/exec/js262.rs | 7 +- boa_tester/src/exec/mod.rs | 91 +++++++++++------- boa_tester/src/main.rs | 25 +++-- boa_tester/src/read.rs | 49 +++++----- boa_wasm/src/lib.rs | 4 +- 34 files changed, 458 insertions(+), 265 deletions(-) create mode 100644 boa_parser/src/source.rs diff --git a/Cargo.lock b/Cargo.lock index 9ae1ffa99f..85541fda92 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -323,7 +323,6 @@ dependencies = [ "bitflags", "boa_engine", "boa_gc", - "boa_parser", "clap 4.1.4", "color-eyre", "colored", diff --git a/boa_cli/src/main.rs b/boa_cli/src/main.rs index eb7c25e518..f5b638950f 100644 --- a/boa_cli/src/main.rs +++ b/boa_cli/src/main.rs @@ -65,7 +65,7 @@ use boa_engine::{ context::ContextBuilder, job::{JobQueue, NativeJob}, vm::flowgraph::{Direction, Graph}, - Context, JsResult, + Context, JsResult, Source, }; use clap::{Parser, ValueEnum, ValueHint}; use colored::{Color, Colorize}; @@ -189,23 +189,22 @@ enum FlowgraphDirection { /// /// Returns a error of type String with a message, /// if the token stream has a parsing error. -fn parse_tokens(src: S, context: &mut Context<'_>) -> Result +fn parse_tokens(src: &S, context: &mut Context<'_>) -> Result where - S: AsRef<[u8]>, + S: AsRef<[u8]> + ?Sized, { - let src_bytes = src.as_ref(); - boa_parser::Parser::new(src_bytes) + boa_parser::Parser::new(Source::from_bytes(&src)) .parse_all(context.interner_mut()) - .map_err(|e| format!("ParsingError: {e}")) + .map_err(|e| format!("Uncaught SyntaxError: {e}")) } /// Dumps the AST to stdout with format controlled by the given arguments. /// /// Returns a error of type String with a error message, /// if the source has a syntax or parsing error. -fn dump(src: S, args: &Opt, context: &mut Context<'_>) -> Result<(), String> +fn dump(src: &S, args: &Opt, context: &mut Context<'_>) -> Result<(), String> where - S: AsRef<[u8]>, + S: AsRef<[u8]> + ?Sized, { if let Some(ref arg) = args.dump_ast { let ast = parse_tokens(src, context)?; @@ -233,7 +232,7 @@ fn generate_flowgraph( format: FlowgraphFormat, direction: Option, ) -> JsResult { - let ast = context.parse(src)?; + let ast = context.parse(Source::from_bytes(src))?; let code = context.compile(&ast)?; let direction = match direction { @@ -279,7 +278,7 @@ fn main() -> Result<(), io::Error> { Err(v) => eprintln!("Uncaught {v}"), } } else { - match context.eval(&buffer) { + match context.eval(Source::from_bytes(&buffer)) { Ok(v) => println!("{}", v.display()), Err(v) => eprintln!("Uncaught {v}"), } @@ -336,7 +335,7 @@ fn main() -> Result<(), io::Error> { Err(v) => eprintln!("Uncaught {v}"), } } else { - match context.eval(line.trim_end()) { + match context.eval(Source::from_bytes(line.trim_end())) { Ok(v) => { println!("{}", v.display()); } diff --git a/boa_engine/benches/full.rs b/boa_engine/benches/full.rs index 043c215946..7802907796 100644 --- a/boa_engine/benches/full.rs +++ b/boa_engine/benches/full.rs @@ -1,6 +1,6 @@ //! Benchmarks of the whole execution engine in Boa. -use boa_engine::{realm::Realm, Context}; +use boa_engine::{realm::Realm, Context, Source}; use criterion::{criterion_group, criterion_main, Criterion}; use std::hint::black_box; @@ -23,7 +23,7 @@ macro_rules! full_benchmarks { static CODE: &str = include_str!(concat!("bench_scripts/", stringify!($name), ".js")); let mut context = Context::default(); c.bench_function(concat!($id, " (Parser)"), move |b| { - b.iter(|| context.parse(black_box(CODE))) + b.iter(|| context.parse(black_box(Source::from_bytes(CODE)))) }); } )* @@ -33,7 +33,7 @@ macro_rules! full_benchmarks { { static CODE: &str = include_str!(concat!("bench_scripts/", stringify!($name), ".js")); let mut context = Context::default(); - let statement_list = context.parse(CODE).expect("parsing failed"); + let statement_list = context.parse(Source::from_bytes(CODE)).expect("parsing failed"); c.bench_function(concat!($id, " (Compiler)"), move |b| { b.iter(|| { context.compile(black_box(&statement_list)) @@ -47,7 +47,7 @@ macro_rules! full_benchmarks { { static CODE: &str = include_str!(concat!("bench_scripts/", stringify!($name), ".js")); let mut context = Context::default(); - let statement_list = context.parse(CODE).expect("parsing failed"); + let statement_list = context.parse(Source::from_bytes(CODE)).expect("parsing failed"); let code_block = context.compile(&statement_list).unwrap(); c.bench_function(concat!($id, " (Execution)"), move |b| { b.iter(|| { diff --git a/boa_engine/src/builtins/array/tests.rs b/boa_engine/src/builtins/array/tests.rs index 3944623dac..91e865e1c7 100644 --- a/boa_engine/src/builtins/array/tests.rs +++ b/boa_engine/src/builtins/array/tests.rs @@ -1,3 +1,5 @@ +use boa_parser::Source; + use super::Array; use crate::builtins::Number; use crate::{forward, Context, JsValue}; @@ -10,60 +12,85 @@ fn is_array() { var new_arr = new Array(); var many = ["a", "b", "c"]; "#; - context.eval(init).unwrap(); + context.eval(Source::from_bytes(init)).unwrap(); assert_eq!( - context.eval("Array.isArray(empty)").unwrap(), + context + .eval(Source::from_bytes("Array.isArray(empty)")) + .unwrap(), JsValue::new(true) ); assert_eq!( - context.eval("Array.isArray(new_arr)").unwrap(), + context + .eval(Source::from_bytes("Array.isArray(new_arr)")) + .unwrap(), JsValue::new(true) ); assert_eq!( - context.eval("Array.isArray(many)").unwrap(), + context + .eval(Source::from_bytes("Array.isArray(many)")) + .unwrap(), JsValue::new(true) ); assert_eq!( - context.eval("Array.isArray([1, 2, 3])").unwrap(), + context + .eval(Source::from_bytes("Array.isArray([1, 2, 3])")) + .unwrap(), JsValue::new(true) ); assert_eq!( - context.eval("Array.isArray([])").unwrap(), + context + .eval(Source::from_bytes("Array.isArray([])")) + .unwrap(), JsValue::new(true) ); assert_eq!( - context.eval("Array.isArray({})").unwrap(), + context + .eval(Source::from_bytes("Array.isArray({})")) + .unwrap(), JsValue::new(false) ); - // assert_eq!(context.eval("Array.isArray(new Array)"), "true"); assert_eq!( - context.eval("Array.isArray()").unwrap(), + context + .eval(Source::from_bytes("Array.isArray(new Array)")) + .unwrap(), + JsValue::new(true) + ); + assert_eq!( + context.eval(Source::from_bytes("Array.isArray()")).unwrap(), JsValue::new(false) ); assert_eq!( context - .eval("Array.isArray({ constructor: Array })") + .eval(Source::from_bytes("Array.isArray({ constructor: Array })")) .unwrap(), JsValue::new(false) ); assert_eq!( context - .eval("Array.isArray({ push: Array.prototype.push, concat: Array.prototype.concat })") + .eval(Source::from_bytes( + "Array.isArray({ push: Array.prototype.push, concat: Array.prototype.concat })" + )) .unwrap(), JsValue::new(false) ); assert_eq!( - context.eval("Array.isArray(17)").unwrap(), + context + .eval(Source::from_bytes("Array.isArray(17)")) + .unwrap(), JsValue::new(false) ); assert_eq!( context - .eval("Array.isArray({ __proto__: Array.prototype })") + .eval(Source::from_bytes( + "Array.isArray({ __proto__: Array.prototype })" + )) .unwrap(), JsValue::new(false) ); assert_eq!( - context.eval("Array.isArray({ length: 0 })").unwrap(), + context + .eval(Source::from_bytes("Array.isArray({ length: 0 })")) + .unwrap(), JsValue::new(false) ); } @@ -73,48 +100,68 @@ fn of() { let mut context = Context::default(); assert_eq!( context - .eval("Array.of(1, 2, 3)") + .eval(Source::from_bytes("Array.of(1, 2, 3)")) .unwrap() .to_string(&mut context) .unwrap(), context - .eval("[1, 2, 3]") + .eval(Source::from_bytes("[1, 2, 3]")) .unwrap() .to_string(&mut context) .unwrap() ); assert_eq!( context - .eval("Array.of(1, 'a', [], undefined, null)") + .eval(Source::from_bytes("Array.of(1, 'a', [], undefined, null)")) .unwrap() .to_string(&mut context) .unwrap(), context - .eval("[1, 'a', [], undefined, null]") + .eval(Source::from_bytes("[1, 'a', [], undefined, null]")) .unwrap() .to_string(&mut context) .unwrap() ); assert_eq!( context - .eval("Array.of()") + .eval(Source::from_bytes("Array.of()")) .unwrap() .to_string(&mut context) .unwrap(), - context.eval("[]").unwrap().to_string(&mut context).unwrap() + context + .eval(Source::from_bytes("[]")) + .unwrap() + .to_string(&mut context) + .unwrap() ); context - .eval(r#"let a = Array.of.call(Date, "a", undefined, 3);"#) + .eval(Source::from_bytes( + r#"let a = Array.of.call(Date, "a", undefined, 3);"#, + )) .unwrap(); assert_eq!( - context.eval("a instanceof Date").unwrap(), + context + .eval(Source::from_bytes("a instanceof Date")) + .unwrap(), JsValue::new(true) ); - assert_eq!(context.eval("a[0]").unwrap(), JsValue::new("a")); - assert_eq!(context.eval("a[1]").unwrap(), JsValue::undefined()); - assert_eq!(context.eval("a[2]").unwrap(), JsValue::new(3)); - assert_eq!(context.eval("a.length").unwrap(), JsValue::new(3)); + assert_eq!( + context.eval(Source::from_bytes("a[0]")).unwrap(), + JsValue::new("a") + ); + assert_eq!( + context.eval(Source::from_bytes("a[1]")).unwrap(), + JsValue::undefined() + ); + assert_eq!( + context.eval(Source::from_bytes("a[2]")).unwrap(), + JsValue::new(3) + ); + assert_eq!( + context.eval(Source::from_bytes("a.length")).unwrap(), + JsValue::new(3) + ); } #[test] @@ -124,31 +171,31 @@ fn concat() { var empty = []; var one = [1]; "#; - context.eval(init).unwrap(); + context.eval(Source::from_bytes(init)).unwrap(); // Empty ++ Empty let ee = context - .eval("empty.concat(empty)") + .eval(Source::from_bytes("empty.concat(empty)")) .unwrap() .display() .to_string(); assert_eq!(ee, "[]"); // Empty ++ NonEmpty let en = context - .eval("empty.concat(one)") + .eval(Source::from_bytes("empty.concat(one)")) .unwrap() .display() .to_string(); assert_eq!(en, "[ 1 ]"); // NonEmpty ++ Empty let ne = context - .eval("one.concat(empty)") + .eval(Source::from_bytes("one.concat(empty)")) .unwrap() .display() .to_string(); assert_eq!(ne, "[ 1 ]"); // NonEmpty ++ NonEmpty let nn = context - .eval("one.concat(one)") + .eval(Source::from_bytes("one.concat(one)")) .unwrap() .display() .to_string(); diff --git a/boa_engine/src/builtins/eval/mod.rs b/boa_engine/src/builtins/eval/mod.rs index 27c0c9e893..7e4146efbb 100644 --- a/boa_engine/src/builtins/eval/mod.rs +++ b/boa_engine/src/builtins/eval/mod.rs @@ -22,7 +22,7 @@ use boa_ast::operations::{ contains, contains_arguments, top_level_var_declared_names, ContainsSymbol, }; use boa_gc::Gc; -use boa_parser::Parser; +use boa_parser::{Parser, Source}; use boa_profiler::Profiler; #[derive(Debug, Clone, Copy)] @@ -124,7 +124,7 @@ impl Eval { // b. If script is a List of errors, throw a SyntaxError exception. // c. If script Contains ScriptBody is false, return undefined. // d. Let body be the ScriptBody of script. - let mut parser = Parser::new(x.as_bytes()); + let mut parser = Parser::new(Source::from_bytes(&x)); if strict { parser.set_strict(); } diff --git a/boa_engine/src/builtins/function/mod.rs b/boa_engine/src/builtins/function/mod.rs index 9964005603..294a321218 100644 --- a/boa_engine/src/builtins/function/mod.rs +++ b/boa_engine/src/builtins/function/mod.rs @@ -34,7 +34,7 @@ use boa_ast::{ }; use boa_gc::{self, custom_trace, Finalize, Gc, Trace}; use boa_interner::Sym; -use boa_parser::Parser; +use boa_parser::{Parser, Source}; use boa_profiler::Profiler; use tap::{Conv, Pipe}; @@ -512,16 +512,17 @@ impl BuiltInFunctionObject { parameters.push(u16::from(b')')); // TODO: make parser generic to u32 iterators - let parameters = match Parser::new(String::from_utf16_lossy(¶meters).as_bytes()) - .parse_formal_parameters(context.interner_mut(), generator, r#async) - { - Ok(parameters) => parameters, - Err(e) => { - return Err(JsNativeError::syntax() - .with_message(format!("failed to parse function parameters: {e}")) - .into()) - } - }; + let parameters = + match Parser::new(Source::from_bytes(&String::from_utf16_lossy(¶meters))) + .parse_formal_parameters(context.interner_mut(), generator, r#async) + { + Ok(parameters) => parameters, + Err(e) => { + return Err(JsNativeError::syntax() + .with_message(format!("failed to parse function parameters: {e}")) + .into()) + } + }; if generator && contains(¶meters, ContainsSymbol::YieldExpression) { return Err(JsNativeError::syntax().with_message( @@ -549,7 +550,7 @@ impl BuiltInFunctionObject { let body_arg = body_arg.to_string(context)?; // TODO: make parser generic to u32 iterators - let body = match Parser::new(body_arg.to_std_string_escaped().as_bytes()) + let body = match Parser::new(Source::from_bytes(&body_arg.to_std_string_escaped())) .parse_function_body(context.interner_mut(), generator, r#async) { Ok(statement_list) => statement_list, diff --git a/boa_engine/src/builtins/json/mod.rs b/boa_engine/src/builtins/json/mod.rs index 832cee8cd8..b43e38ebaa 100644 --- a/boa_engine/src/builtins/json/mod.rs +++ b/boa_engine/src/builtins/json/mod.rs @@ -30,7 +30,7 @@ use crate::{ value::IntegerOrInfinity, Context, JsResult, JsString, JsValue, }; -use boa_parser::Parser; +use boa_parser::{Parser, Source}; use boa_profiler::Profiler; use tap::{Conv, Pipe}; @@ -198,7 +198,7 @@ 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 mut parser = Parser::new(script_string.as_bytes()); + let mut parser = Parser::new(Source::from_bytes(&script_string)); parser.set_json_parse(); let statement_list = parser.parse_all(context.interner_mut())?; let code_block = context.compile_json_parse(&statement_list)?; diff --git a/boa_engine/src/builtins/object/tests.rs b/boa_engine/src/builtins/object/tests.rs index cc7bd37fe3..1d094c636a 100644 --- a/boa_engine/src/builtins/object/tests.rs +++ b/boa_engine/src/builtins/object/tests.rs @@ -1,3 +1,5 @@ +use boa_parser::Source; + use crate::{check_output, forward, Context, JsValue, TestAction}; #[test] @@ -249,7 +251,10 @@ fn get_own_property_descriptor_1_arg_returns_undefined() { let obj = {a: 2}; Object.getOwnPropertyDescriptor(obj) "#; - assert_eq!(context.eval(code).unwrap(), JsValue::undefined()); + assert_eq!( + context.eval(Source::from_bytes(code)).unwrap(), + JsValue::undefined() + ); } #[test] @@ -318,7 +323,10 @@ fn object_is_prototype_of() { Object.prototype.isPrototypeOf(String.prototype) "#; - assert_eq!(context.eval(init).unwrap(), JsValue::new(true)); + assert_eq!( + context.eval(Source::from_bytes(init)).unwrap(), + JsValue::new(true) + ); } #[test] diff --git a/boa_engine/src/builtins/promise/tests.rs b/boa_engine/src/builtins/promise/tests.rs index 9886ab5e87..e121e1b9f3 100644 --- a/boa_engine/src/builtins/promise/tests.rs +++ b/boa_engine/src/builtins/promise/tests.rs @@ -1,3 +1,5 @@ +use boa_parser::Source; + use crate::{context::ContextBuilder, forward, job::SimpleJobQueue}; #[test] @@ -13,7 +15,7 @@ fn promise() { count += 1; count; "#; - let result = context.eval(init).unwrap(); + let result = context.eval(Source::from_bytes(init)).unwrap(); assert_eq!(result.as_number(), Some(2_f64)); context.run_jobs(); let after_completion = forward(&mut context, "count"); diff --git a/boa_engine/src/builtins/weak/weak_ref.rs b/boa_engine/src/builtins/weak/weak_ref.rs index 32cf0a52f4..75695920d7 100644 --- a/boa_engine/src/builtins/weak/weak_ref.rs +++ b/boa_engine/src/builtins/weak/weak_ref.rs @@ -141,6 +141,8 @@ impl WeakRef { #[cfg(test)] mod tests { + use boa_parser::Source; + use crate::{Context, JsValue}; #[test] @@ -148,7 +150,7 @@ mod tests { let context = &mut Context::default(); assert!(context - .eval( + .eval(Source::from_bytes( r#" var ptr; { @@ -157,7 +159,7 @@ mod tests { } ptr.deref() "# - ) + )) .unwrap() .is_object()); @@ -165,11 +167,11 @@ mod tests { assert_eq!( context - .eval( + .eval(Source::from_bytes( r#" ptr.deref() "# - ) + )) .unwrap(), JsValue::undefined() ); diff --git a/boa_engine/src/context/hooks.rs b/boa_engine/src/context/hooks.rs index bafa7963d7..60ebac06e0 100644 --- a/boa_engine/src/context/hooks.rs +++ b/boa_engine/src/context/hooks.rs @@ -16,7 +16,7 @@ use crate::{ /// need to be redefined: /// /// ``` -/// use boa_engine::{JsNativeError, JsResult, context::{Context, ContextBuilder, HostHooks}}; +/// use boa_engine::{JsNativeError, JsResult, context::{Context, ContextBuilder, HostHooks}, Source}; /// /// struct Hooks; /// @@ -30,7 +30,7 @@ use crate::{ /// } /// let hooks = Hooks; // Can have additional state. /// let context = &mut ContextBuilder::new().host_hooks(&hooks).build(); -/// let result = context.eval(r#"eval("let a = 5")"#); +/// let result = context.eval(Source::from_bytes(r#"eval("let a = 5")"#)); /// assert_eq!(result.unwrap_err().to_string(), "TypeError: eval calls not available"); /// ``` /// diff --git a/boa_engine/src/context/mod.rs b/boa_engine/src/context/mod.rs index e45c421146..9f9c9ecf61 100644 --- a/boa_engine/src/context/mod.rs +++ b/boa_engine/src/context/mod.rs @@ -13,6 +13,7 @@ pub use icu::BoaProvider; use intrinsics::{IntrinsicObjects, Intrinsics}; +use std::io::Read; #[cfg(not(feature = "intl"))] pub use std::marker::PhantomData; @@ -28,7 +29,7 @@ use crate::{ property::{Attribute, PropertyDescriptor, PropertyKey}, realm::Realm, vm::{CallFrame, CodeBlock, Vm}, - JsResult, JsValue, + JsResult, JsValue, Source, }; use boa_ast::StatementList; @@ -52,6 +53,7 @@ use boa_profiler::Profiler; /// object::ObjectInitializer, /// property::{Attribute, PropertyDescriptor}, /// Context, +/// Source /// }; /// /// let script = r#" @@ -66,7 +68,7 @@ use boa_profiler::Profiler; /// let mut context = Context::default(); /// /// // Populate the script definition to the context. -/// context.eval(script).unwrap(); +/// context.eval(Source::from_bytes(script)).unwrap(); /// /// // Create an object that can be used in eval calls. /// let arg = ObjectInitializer::new(&mut context) @@ -74,7 +76,7 @@ use boa_profiler::Profiler; /// .build(); /// context.register_global_property("arg", arg, Attribute::all()); /// -/// let value = context.eval("test(arg)").unwrap(); +/// let value = context.eval(Source::from_bytes("test(arg)")).unwrap(); /// /// assert_eq!(value.as_number(), Some(12.0)) /// ``` @@ -92,6 +94,9 @@ pub struct Context<'host> { /// Intrinsic objects intrinsics: Intrinsics, + /// Execute in strict mode, + strict: bool, + /// Number of instructions remaining before a forced exit #[cfg(feature = "fuzz")] pub(crate) instructions_remaining: usize, @@ -118,6 +123,7 @@ impl std::fmt::Debug for Context<'_> { .field("interner", &self.interner) .field("intrinsics", &self.intrinsics) .field("vm", &self.vm) + .field("strict", &self.strict) .field("promise_job_queue", &"JobQueue") .field("hooks", &"HostHooks"); @@ -147,14 +153,16 @@ impl Context<'_> { ContextBuilder::default() } - /// Evaluates the given code by compiling down to bytecode, then interpreting the bytecode into a value + /// Evaluates the given script `Source` by compiling down to bytecode, then interpreting the + /// bytecode into a value. /// /// # Examples /// ``` - /// # use boa_engine::Context; + /// # use boa_engine::{Context, Source}; /// let mut context = Context::default(); /// - /// let value = context.eval("1 + 3").unwrap(); + /// let source = Source::from_bytes("1 + 3"); + /// let value = context.eval(source).unwrap(); /// /// assert!(value.is_number()); /// assert_eq!(value.as_number().unwrap(), 4.0); @@ -163,14 +171,11 @@ impl Context<'_> { /// Note that this won't run any scheduled promise jobs; you need to call [`Context::run_jobs`] /// on the context or [`JobQueue::run_jobs`] on the provided queue to run them. #[allow(clippy::unit_arg, clippy::drop_copy)] - pub fn eval(&mut self, src: S) -> JsResult - where - S: AsRef<[u8]>, - { + pub fn eval(&mut self, src: Source<'_, R>) -> JsResult { let main_timer = Profiler::global().start_event("Evaluation", "Main"); - let statement_list = self.parse(src)?; - let code_block = self.compile(&statement_list)?; + let script = self.parse(src)?; + let code_block = self.compile(&script)?; let result = self.execute(code_block); // The main_timer needs to be dropped before the Profiler is. @@ -180,13 +185,13 @@ impl Context<'_> { result } - /// Parse the given source text. - pub fn parse(&mut self, src: S) -> Result - where - S: AsRef<[u8]>, - { + /// Parse the given source script. + pub fn parse(&mut self, src: Source<'_, R>) -> Result { let _timer = Profiler::global().start_event("Parsing", "Main"); - let mut parser = Parser::new(src.as_ref()); + let mut parser = Parser::new(src); + if self.strict { + parser.set_strict(); + } parser.parse_all(&mut self.interner) } @@ -380,6 +385,11 @@ impl Context<'_> { self.vm.trace = trace; } + /// Executes all code in strict mode. + pub fn strict(&mut self, strict: bool) { + self.strict = strict; + } + /// Enqueues a [`NativeJob`] on the [`JobQueue`]. pub fn enqueue_job(&mut self, job: NativeJob) { self.job_queue.enqueue_promise_job(job, self); @@ -605,6 +615,7 @@ impl<'icu, 'hooks, 'queue> ContextBuilder<'icu, 'hooks, 'queue> { console: Console::default(), intrinsics, vm: Vm::default(), + strict: false, #[cfg(feature = "intl")] icu: self.icu.unwrap_or_else(|| { let provider = BoaProvider::Buffer(boa_icu_provider::buffer()); diff --git a/boa_engine/src/lib.rs b/boa_engine/src/lib.rs index 11ebecbb2c..9368601267 100644 --- a/boa_engine/src/lib.rs +++ b/boa_engine/src/lib.rs @@ -135,6 +135,7 @@ pub mod prelude { object::JsObject, Context, JsBigInt, JsResult, JsString, JsValue, }; + pub use boa_parser::Source; } use std::result::Result as StdResult; @@ -149,6 +150,8 @@ pub use crate::{ symbol::JsSymbol, value::JsValue, }; +#[doc(inline)] +pub use boa_parser::Source; /// The result of a Javascript expression is represented like this so it can succeed (`Ok`) or fail (`Err`) pub type JsResult = StdResult; @@ -157,12 +160,12 @@ pub type JsResult = StdResult; /// /// The state of the `Context` is changed, and a string representation of the result is returned. #[cfg(test)] -pub(crate) fn forward(context: &mut Context<'_>, src: S) -> String +pub(crate) fn forward(context: &mut Context<'_>, src: &S) -> String where - S: AsRef<[u8]>, + S: AsRef<[u8]> + ?Sized, { context - .eval(src.as_ref()) + .eval(Source::from_bytes(src)) .map_or_else(|e| format!("Uncaught {e}"), |v| v.display().to_string()) } @@ -172,13 +175,15 @@ where /// If the interpreter fails parsing an error value is returned instead (error object) #[allow(clippy::unit_arg, clippy::drop_copy)] #[cfg(test)] -pub(crate) fn forward_val>(context: &mut Context<'_>, src: T) -> JsResult { +pub(crate) fn forward_val + ?Sized>( + context: &mut Context<'_>, + src: &T, +) -> JsResult { use boa_profiler::Profiler; let main_timer = Profiler::global().start_event("Main", "Main"); - let src_bytes: &[u8] = src.as_ref(); - let result = context.eval(src_bytes); + let result = context.eval(Source::from_bytes(src)); // The main_timer needs to be dropped before the Profiler is. drop(main_timer); @@ -189,10 +194,8 @@ pub(crate) fn forward_val>(context: &mut Context<'_>, src: T) -> /// Create a clean Context and execute the code #[cfg(test)] -pub(crate) fn exec>(src: T) -> String { - let src_bytes: &[u8] = src.as_ref(); - - match Context::default().eval(src_bytes) { +pub(crate) fn exec + ?Sized>(src: &T) -> String { + match Context::default().eval(Source::from_bytes(src)) { Ok(value) => value.display().to_string(), Err(error) => error.to_string(), } diff --git a/boa_engine/src/tests.rs b/boa_engine/src/tests.rs index 5fac76f7d0..c14fbea292 100644 --- a/boa_engine/src/tests.rs +++ b/boa_engine/src/tests.rs @@ -1,3 +1,5 @@ +use boa_parser::Source; + use crate::{ builtins::Number, check_output, exec, forward, forward_val, string::utf16, value::IntegerOrInfinity, Context, JsValue, TestAction, @@ -454,7 +456,7 @@ fn test_invalid_break_target() { } "#; - assert!(Context::default().eval(src).is_err()); + assert!(Context::default().eval(Source::from_bytes(src)).is_err()); } #[test] @@ -2091,7 +2093,7 @@ fn bigger_switch_example() { "#, ); - assert_eq!(&exec(scenario), val); + assert_eq!(&exec(&scenario), val); } } diff --git a/boa_engine/src/value/serde_json.rs b/boa_engine/src/value/serde_json.rs index b41c1c866e..7f49947d23 100644 --- a/boa_engine/src/value/serde_json.rs +++ b/boa_engine/src/value/serde_json.rs @@ -168,6 +168,8 @@ impl JsValue { #[cfg(test)] mod tests { + use boa_parser::Source; + use crate::object::JsArray; use crate::{Context, JsValue}; @@ -225,61 +227,61 @@ mod tests { let mut context = Context::default(); let add = context - .eval( + .eval(Source::from_bytes( r#" 1000000 + 500 "#, - ) + )) .unwrap(); let add: u32 = serde_json::from_value(add.to_json(&mut context).unwrap()).unwrap(); assert_eq!(add, 1_000_500); let sub = context - .eval( + .eval(Source::from_bytes( r#" 1000000 - 500 "#, - ) + )) .unwrap(); let sub: u32 = serde_json::from_value(sub.to_json(&mut context).unwrap()).unwrap(); assert_eq!(sub, 999_500); let mult = context - .eval( + .eval(Source::from_bytes( r#" 1000000 * 500 "#, - ) + )) .unwrap(); let mult: u32 = serde_json::from_value(mult.to_json(&mut context).unwrap()).unwrap(); assert_eq!(mult, 500_000_000); let div = context - .eval( + .eval(Source::from_bytes( r#" 1000000 / 500 "#, - ) + )) .unwrap(); let div: u32 = serde_json::from_value(div.to_json(&mut context).unwrap()).unwrap(); assert_eq!(div, 2000); let rem = context - .eval( + .eval(Source::from_bytes( r#" 233894 % 500 "#, - ) + )) .unwrap(); let rem: u32 = serde_json::from_value(rem.to_json(&mut context).unwrap()).unwrap(); assert_eq!(rem, 394); let pow = context - .eval( + .eval(Source::from_bytes( r#" 36 ** 5 "#, - ) + )) .unwrap(); let pow: u32 = serde_json::from_value(pow.to_json(&mut context).unwrap()).unwrap(); diff --git a/boa_engine/src/vm/tests.rs b/boa_engine/src/vm/tests.rs index 106c17001c..d1dd2cca2b 100644 --- a/boa_engine/src/vm/tests.rs +++ b/boa_engine/src/vm/tests.rs @@ -1,3 +1,5 @@ +use boa_parser::Source; + use crate::{check_output, exec, Context, JsValue, TestAction}; #[test] @@ -45,7 +47,7 @@ fn try_catch_finally_from_init() { assert_eq!( Context::default() - .eval(source.as_bytes()) + .eval(Source::from_bytes(source)) .unwrap_err() .as_opaque() .unwrap(), @@ -68,7 +70,7 @@ fn multiple_catches() { "#; assert_eq!( - Context::default().eval(source.as_bytes()).unwrap(), + Context::default().eval(Source::from_bytes(source)).unwrap(), JsValue::Undefined ); } @@ -87,7 +89,7 @@ fn use_last_expr_try_block() { "#; assert_eq!( - Context::default().eval(source.as_bytes()).unwrap(), + Context::default().eval(Source::from_bytes(source)).unwrap(), JsValue::from("Hello!") ); } @@ -105,7 +107,7 @@ fn use_last_expr_catch_block() { "#; assert_eq!( - Context::default().eval(source.as_bytes()).unwrap(), + Context::default().eval(Source::from_bytes(source)).unwrap(), JsValue::from("Hello!") ); } @@ -121,7 +123,7 @@ fn no_use_last_expr_finally_block() { "#; assert_eq!( - Context::default().eval(source.as_bytes()).unwrap(), + Context::default().eval(Source::from_bytes(source)).unwrap(), JsValue::undefined() ); } @@ -140,7 +142,7 @@ fn finally_block_binding_env() { "#; assert_eq!( - Context::default().eval(source.as_bytes()).unwrap(), + Context::default().eval(Source::from_bytes(source)).unwrap(), JsValue::from("Hey hey people") ); } @@ -159,7 +161,7 @@ fn run_super_method_in_object() { "#; assert_eq!( - Context::default().eval(source.as_bytes()).unwrap(), + Context::default().eval(Source::from_bytes(source)).unwrap(), JsValue::from("super") ); } @@ -185,7 +187,7 @@ fn get_reference_by_super() { "#; assert_eq!( - Context::default().eval(source.as_bytes()).unwrap(), + Context::default().eval(Source::from_bytes(source)).unwrap(), JsValue::from("ab") ); } diff --git a/boa_examples/src/bin/classes.rs b/boa_examples/src/bin/classes.rs index b1e7fd057a..20b657a9d9 100644 --- a/boa_examples/src/bin/classes.rs +++ b/boa_examples/src/bin/classes.rs @@ -4,7 +4,7 @@ use boa_engine::{ class::{Class, ClassBuilder}, error::JsNativeError, property::Attribute, - Context, JsResult, JsString, JsValue, + Context, JsResult, JsString, JsValue, Source, }; use boa_gc::{Finalize, Trace}; @@ -130,7 +130,7 @@ fn main() { // Having done all of that, we can execute Javascript code with `eval`, // and access the `Person` class defined in Rust! context - .eval( + .eval(Source::from_bytes( r" let person = new Person('John', 19); person.sayHello(); @@ -146,6 +146,6 @@ fn main() { console.log(person.inheritedProperty); console.log(Person.prototype.inheritedProperty === person.inheritedProperty); ", - ) + )) .unwrap(); } diff --git a/boa_examples/src/bin/closures.rs b/boa_examples/src/bin/closures.rs index 44fdfde683..7b396cba80 100644 --- a/boa_examples/src/bin/closures.rs +++ b/boa_examples/src/bin/closures.rs @@ -9,7 +9,7 @@ use boa_engine::{ object::{builtins::JsArray, FunctionObjectBuilder, JsObject}, property::{Attribute, PropertyDescriptor}, string::utf16, - Context, JsError, JsNativeError, JsString, JsValue, + Context, JsError, JsNativeError, JsString, JsValue, Source, }; use boa_gc::{Finalize, GcRefCell, Trace}; @@ -36,7 +36,7 @@ fn main() -> Result<(), JsError> { }), ); - assert_eq!(context.eval("closure()")?, 255.into()); + assert_eq!(context.eval(Source::from_bytes("closure()"))?, 255.into()); // We have created a closure with moved variables and executed that closure // inside Javascript! @@ -117,13 +117,13 @@ fn main() -> Result<(), JsError> { ); assert_eq!( - context.eval("createMessage()")?, + context.eval(Source::from_bytes("createMessage()"))?, "message from `Boa dev`: Hello!".into() ); // The data mutates between calls assert_eq!( - context.eval("createMessage(); createMessage();")?, + context.eval(Source::from_bytes("createMessage(); createMessage();"))?, "message from `Boa dev`: Hello! Hello! Hello!".into() ); @@ -167,7 +167,7 @@ fn main() -> Result<(), JsError> { ); // First call should return the array `[0]`. - let result = context.eval("enumerate()")?; + let result = context.eval(Source::from_bytes("enumerate()"))?; let object = result .as_object() .cloned() @@ -178,7 +178,7 @@ fn main() -> Result<(), JsError> { assert_eq!(array.get(1, &mut context)?, JsValue::undefined()); // First call should return the array `[0, 1]`. - let result = context.eval("enumerate()")?; + let result = context.eval(Source::from_bytes("enumerate()"))?; let object = result .as_object() .cloned() diff --git a/boa_examples/src/bin/commuter_visitor.rs b/boa_examples/src/bin/commuter_visitor.rs index 2668b69c20..6f1a59c7ab 100644 --- a/boa_examples/src/bin/commuter_visitor.rs +++ b/boa_examples/src/bin/commuter_visitor.rs @@ -10,11 +10,11 @@ use boa_ast::{ visitor::{VisitWith, VisitorMut}, Expression, }; -use boa_engine::Context; +use boa_engine::{Context, Source}; use boa_interner::ToInternedString; use boa_parser::Parser; use core::ops::ControlFlow; -use std::{convert::Infallible, fs::File, io::BufReader}; +use std::{convert::Infallible, path::Path}; /// Visitor which, when applied to a binary expression, will swap the operands. Use in other /// circumstances is undefined. @@ -65,9 +65,8 @@ impl<'ast> VisitorMut<'ast> for CommutorVisitor { } fn main() { - let mut parser = Parser::new(BufReader::new( - File::open("boa_examples/scripts/calc.js").unwrap(), - )); + let mut parser = + Parser::new(Source::from_filepath(Path::new("boa_examples/scripts/calc.js")).unwrap()); let mut ctx = Context::default(); let mut statements = parser.parse_all(ctx.interner_mut()).unwrap(); diff --git a/boa_examples/src/bin/loadfile.rs b/boa_examples/src/bin/loadfile.rs index 56ef6d5004..72a0784bd0 100644 --- a/boa_examples/src/bin/loadfile.rs +++ b/boa_examples/src/bin/loadfile.rs @@ -1,14 +1,14 @@ // This example shows how to load, parse and execute JS code from a source file // (./scripts/helloworld.js) -use std::fs; +use std::path::Path; -use boa_engine::Context; +use boa_engine::{Context, Source}; fn main() { let js_file_path = "./scripts/helloworld.js"; - match fs::read(js_file_path) { + match Source::from_filepath(Path::new(js_file_path)) { Ok(src) => { // Instantiate the execution context let mut context = Context::default(); diff --git a/boa_examples/src/bin/loadstring.rs b/boa_examples/src/bin/loadstring.rs index 5a47cfb2a9..4894f9625f 100644 --- a/boa_examples/src/bin/loadstring.rs +++ b/boa_examples/src/bin/loadstring.rs @@ -1,6 +1,6 @@ // This example loads, parses and executes a JS code string -use boa_engine::Context; +use boa_engine::{Context, Source}; fn main() { let js_code = "console.log('Hello World from a JS code string!')"; @@ -9,7 +9,7 @@ fn main() { let mut context = Context::default(); // Parse the source code - match context.eval(js_code) { + match context.eval(Source::from_bytes(js_code)) { Ok(res) => { println!( "{}", diff --git a/boa_examples/src/bin/modulehandler.rs b/boa_examples/src/bin/modulehandler.rs index ff0cc63d8a..f3214b6eee 100644 --- a/boa_examples/src/bin/modulehandler.rs +++ b/boa_examples/src/bin/modulehandler.rs @@ -3,7 +3,7 @@ use boa_engine::{ native_function::NativeFunction, prelude::JsObject, property::Attribute, Context, JsResult, - JsValue, + JsValue, Source, }; use std::fs::read_to_string; @@ -31,7 +31,7 @@ fn main() { // Instantiating the engine with the execution context // Loading, parsing and executing the JS code from the source file - ctx.eval(&buffer.unwrap()).unwrap(); + ctx.eval(Source::from_bytes(&buffer.unwrap())).unwrap(); } // Custom implementation that mimics the 'require' module loader @@ -52,7 +52,7 @@ fn require(_: &JsValue, args: &[JsValue], ctx: &mut Context<'_>) -> JsResult Visitor<'ast> for SymbolVisitor { } fn main() { - let mut parser = Parser::new(BufReader::new( - File::open("boa_examples/scripts/calc.js").unwrap(), - )); + let mut parser = + Parser::new(Source::from_filepath(Path::new("boa_examples/scripts/calc.js")).unwrap()); let mut ctx = Context::default(); let statements = parser.parse_all(ctx.interner_mut()).unwrap(); diff --git a/boa_parser/src/lib.rs b/boa_parser/src/lib.rs index 9c3043a33d..df92ae5d1d 100644 --- a/boa_parser/src/lib.rs +++ b/boa_parser/src/lib.rs @@ -95,7 +95,9 @@ pub mod error; pub mod lexer; pub mod parser; +mod source; pub use error::Error; pub use lexer::Lexer; pub use parser::Parser; +pub use source::Source; diff --git a/boa_parser/src/parser/mod.rs b/boa_parser/src/parser/mod.rs index 18363fdc0b..c9c5bc5e67 100644 --- a/boa_parser/src/parser/mod.rs +++ b/boa_parser/src/parser/mod.rs @@ -15,7 +15,7 @@ use crate::{ cursor::Cursor, function::{FormalParameters, FunctionStatementList}, }, - Error, + Error, Source, }; use boa_ast::{ expression::Identifier, @@ -27,7 +27,7 @@ use boa_ast::{ }; use boa_interner::Interner; use rustc_hash::FxHashSet; -use std::io::Read; +use std::{io::Read, path::Path}; /// Trait implemented by parsers. /// @@ -105,38 +105,23 @@ impl From for AllowDefault { /// [label]: https://tc39.es/ecma262/#sec-labelled-function-declarations /// [block]: https://tc39.es/ecma262/#sec-block-duplicates-allowed-static-semantics #[derive(Debug)] -pub struct Parser { +#[allow(unused)] // Right now the path is not used, but it's better to have it for future improvements. +pub struct Parser<'a, R> { + /// Path to the source being parsed. + path: Option<&'a Path>, /// Cursor of the parser, pointing to the lexer and used to get tokens for the parser. cursor: Cursor, } -impl Parser { - /// Create a new `Parser` with a reader as the input to parse. - pub fn new(reader: R) -> Self - where - R: Read, - { +impl<'a, R: Read> Parser<'a, R> { + /// Create a new `Parser` with a `Source` as the input to parse. + pub fn new(source: Source<'a, R>) -> Self { Self { - cursor: Cursor::new(reader), + path: source.path, + cursor: Cursor::new(source.reader), } } - /// Set the parser strict mode to true. - pub fn set_strict(&mut self) - where - R: Read, - { - self.cursor.set_strict_mode(true); - } - - /// Set the parser strict mode to true. - 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. /// @@ -145,10 +130,7 @@ impl Parser { /// Will return `Err` on any parsing error, including invalid reads of the bytes being parsed. /// /// [spec]: https://tc39.es/ecma262/#prod-Script - pub fn parse_all(&mut self, interner: &mut Interner) -> ParseResult - where - R: Read, - { + pub fn parse_all(&mut self, interner: &mut Interner) -> ParseResult { Script::new(false).parse(&mut self.cursor, interner) } @@ -165,10 +147,7 @@ impl Parser { &mut self, direct: bool, interner: &mut Interner, - ) -> ParseResult - where - R: Read, - { + ) -> ParseResult { Script::new(direct).parse(&mut self.cursor, interner) } @@ -184,10 +163,7 @@ impl Parser { interner: &mut Interner, allow_yield: bool, allow_await: bool, - ) -> ParseResult - where - R: Read, - { + ) -> ParseResult { FunctionStatementList::new(allow_yield, allow_await).parse(&mut self.cursor, interner) } @@ -203,11 +179,26 @@ impl Parser { interner: &mut Interner, allow_yield: bool, allow_await: bool, - ) -> ParseResult + ) -> ParseResult { + FormalParameters::new(allow_yield, allow_await).parse(&mut self.cursor, interner) + } +} + +impl Parser<'_, R> { + /// Set the parser strict mode to true. + pub fn set_strict(&mut self) where R: Read, { - FormalParameters::new(allow_yield, allow_await).parse(&mut self.cursor, interner) + self.cursor.set_strict_mode(true); + } + + /// Set the parser JSON mode to true. + pub fn set_json_parse(&mut self) + where + R: Read, + { + self.cursor.set_json_parse(true); } } diff --git a/boa_parser/src/parser/tests/format/mod.rs b/boa_parser/src/parser/tests/format/mod.rs index 0e6b8e13bc..4bb87d5b2f 100644 --- a/boa_parser/src/parser/tests/format/mod.rs +++ b/boa_parser/src/parser/tests/format/mod.rs @@ -16,7 +16,7 @@ mod statement; fn test_formatting(source: &'static str) { // Remove preceding newline. - use crate::Parser; + use crate::{Parser, Source}; use boa_interner::{Interner, ToInternedString}; let source = &source[1..]; @@ -30,8 +30,9 @@ fn test_formatting(source: &'static str) { .map(|l| &l[characters_to_remove..]) // Remove preceding whitespace from each line .collect::>() .join("\n"); + let source = Source::from_bytes(source); let interner = &mut Interner::default(); - let result = Parser::new(scenario.as_bytes()) + let result = Parser::new(source) .parse_all(interner) .expect("parsing failed") .to_interned_string(interner); diff --git a/boa_parser/src/parser/tests/mod.rs b/boa_parser/src/parser/tests/mod.rs index 2085badc07..a6d8aa57b5 100644 --- a/boa_parser/src/parser/tests/mod.rs +++ b/boa_parser/src/parser/tests/mod.rs @@ -4,7 +4,7 @@ mod format; use std::convert::TryInto; -use crate::Parser; +use crate::{Parser, Source}; use boa_ast::{ declaration::{Declaration, LexicalDeclaration, VarDeclaration, Variable}, expression::{ @@ -36,7 +36,7 @@ where L: Into>, { assert_eq!( - Parser::new(js.as_bytes()) + Parser::new(Source::from_bytes(js)) .parse_all(interner) .expect("failed to parse"), StatementList::from(expr.into()) @@ -46,7 +46,7 @@ where /// Checks that the given javascript string creates a parse error. #[track_caller] pub(super) fn check_invalid(js: &str) { - assert!(Parser::new(js.as_bytes()) + assert!(Parser::new(Source::from_bytes(js)) .parse_all(&mut Interner::default()) .is_err()); } diff --git a/boa_parser/src/source.rs b/boa_parser/src/source.rs new file mode 100644 index 0000000000..539c8c8c02 --- /dev/null +++ b/boa_parser/src/source.rs @@ -0,0 +1,88 @@ +use std::{ + fs::File, + io::{self, BufReader, Read}, + path::Path, +}; + +/// A source of ECMAScript code. +/// +/// [`Source`]s can be created from plain [`str`]s, file [`Path`]s or more generally, any [`Read`] +/// instance. +#[derive(Debug)] +pub struct Source<'path, R> { + pub(crate) reader: R, + pub(crate) path: Option<&'path Path>, +} + +impl<'bytes> Source<'static, &'bytes [u8]> { + /// Creates a new `Source` from any type equivalent to a slice of bytes e.g. [`&str`][str], + /// [Vec]<[u8]>, [Box]<[\[u8\]][slice]> or a plain slice + /// [&\[u8\]][slice]. + /// + /// # Examples + /// + /// ``` + /// # use boa_parser::Source; + /// let code = r#"var array = [5, 4, 3, 2, 1];"#; + /// let source = Source::from_bytes(code); + /// ``` + /// + /// [slice]: std::slice + pub fn from_bytes + ?Sized>(source: &'bytes T) -> Self { + Self { + reader: source.as_ref(), + path: None, + } + } +} + +impl<'path> Source<'path, BufReader> { + /// Creates a new `Source` from a `Path` to a file. + /// + /// # Errors + /// + /// See [`File::open`]. + /// + /// # Examples + /// + /// ```no_run + /// # use boa_parser::Source; + /// # use std::{fs::File, path::Path}; + /// # fn main() -> std::io::Result<()> { + /// let path = Path::new("script.js"); + /// let source = Source::from_filepath(path)?; + /// # Ok(()) + /// # } + /// ``` + pub fn from_filepath(source: &'path Path) -> io::Result { + let reader = File::open(source)?; + Ok(Self { + reader: BufReader::new(reader), + path: Some(source), + }) + } +} + +impl<'path, R: Read> Source<'path, R> { + /// Creates a new `Source` from a [`Read`] instance and an optional [`Path`]. + /// + /// # Examples + /// + /// ```no_run + /// # use boa_parser::Source; + /// # use std::{fs::File, io::Read, path::Path}; + /// # fn main() -> std::io::Result<()> { + /// let strictler = r#""use strict""#; + /// + /// let path = Path::new("no_strict.js"); + /// let file = File::open(path)?; + /// let strict = strictler.as_bytes().chain(file); + /// + /// let source = Source::from_reader(strict, Some(path)); + /// # Ok(()) + /// # } + /// ``` + pub const fn from_reader(reader: R, path: Option<&'path Path>) -> Self { + Self { reader, path } + } +} diff --git a/boa_tester/Cargo.toml b/boa_tester/Cargo.toml index 2b1db1ac67..948880248e 100644 --- a/boa_tester/Cargo.toml +++ b/boa_tester/Cargo.toml @@ -14,7 +14,6 @@ rust-version.workspace = true [dependencies] boa_engine.workspace = true boa_gc.workspace = true -boa_parser.workspace = true clap = { version = "4.1.4", features = ["derive"] } serde = { version = "1.0.152", features = ["derive"] } serde_yaml = "0.9.17" diff --git a/boa_tester/src/exec/js262.rs b/boa_tester/src/exec/js262.rs index a690e17cc9..35836cbe60 100644 --- a/boa_tester/src/exec/js262.rs +++ b/boa_tester/src/exec/js262.rs @@ -2,7 +2,7 @@ use boa_engine::{ builtins::JsArgs, object::{JsObject, ObjectInitializer}, property::Attribute, - Context, JsNativeError, JsResult, JsValue, + Context, JsNativeError, JsResult, JsValue, Source, }; /// Initializes the object in the context. @@ -83,14 +83,15 @@ fn detach_array_buffer( fn eval_script(_this: &JsValue, args: &[JsValue], context: &mut Context<'_>) -> JsResult { args.get(0).and_then(JsValue::as_string).map_or_else( || Ok(JsValue::undefined()), - |source_text| match context.parse(source_text.to_std_string_escaped()) { + |source_text| match context.parse(Source::from_bytes(&source_text.to_std_string_escaped())) + { // TODO: check strict Err(e) => Err(JsNativeError::typ() .with_message(format!("Uncaught Syntax Error: {e}")) .into()), // Calling eval here parses the code a second time. // TODO: We can fix this after we have have defined the public api for the vm executer. - Ok(_) => context.eval(source_text.to_std_string_escaped()), + Ok(_) => context.eval(Source::from_bytes(&source_text.to_std_string_escaped())), }, ) } diff --git a/boa_tester/src/exec/mod.rs b/boa_tester/src/exec/mod.rs index d3fac43955..07e2318a0a 100644 --- a/boa_tester/src/exec/mod.rs +++ b/boa_tester/src/exec/mod.rs @@ -9,18 +9,17 @@ use crate::read::ErrorType; use boa_engine::{ builtins::JsArgs, context::ContextBuilder, job::SimpleJobQueue, native_function::NativeFunction, object::FunctionObjectBuilder, property::Attribute, Context, - JsNativeErrorKind, JsValue, + JsNativeErrorKind, JsValue, Source, }; -use boa_parser::Parser; use colored::Colorize; use rayon::prelude::*; -use std::{borrow::Cow, cell::RefCell, rc::Rc}; +use std::{cell::RefCell, rc::Rc}; impl TestSuite { /// Runs the test suite. pub(crate) fn run(&self, harness: &Harness, verbose: u8, parallel: bool) -> SuiteResult { if verbose != 0 { - println!("Suite {}:", self.name); + println!("Suite {}:", self.path.display()); } let suites: Vec<_> = if parallel { @@ -85,7 +84,7 @@ impl TestSuite { println!( "Suite {} results: total: {total}, passed: {}, ignored: {}, failed: {} (panics: \ {}{}), conformance: {:.2}%", - self.name, + self.path.display(), passed.to_string().green(), ignored.to_string().yellow(), (total - passed - ignored).to_string().red(), @@ -129,11 +128,29 @@ impl Test { /// Runs the test once, in strict or non-strict mode fn run_once(&self, harness: &Harness, strict: bool, verbose: u8) -> TestResult { + let Ok(source) = Source::from_filepath(&self.path) else { + if verbose > 1 { + println!( + "`{}`{}: {}", + self.path.display(), + if strict { " (strict mode)" } else { "" }, + "Invalid file".red() + ); + } else { + print!("{}", "F".red()); + } + return TestResult { + name: self.name.clone(), + strict, + result: TestOutcomeResult::Failed, + result_text: Box::from("Could not read test file.") + } + }; if self.ignored { if verbose > 1 { println!( "`{}`{}: {}", - self.name, + self.path.display(), if strict { " (strict mode)" } else { "" }, "Ignored".yellow() ); @@ -150,17 +167,11 @@ impl Test { if verbose > 1 { println!( "`{}`{}: starting", - self.name, + self.path.display(), if strict { " (strict mode)" } else { "" } ); } - let test_content = if strict { - Cow::Owned(format!("\"use strict\";\n{}", self.content)) - } else { - Cow::Borrowed(&*self.content) - }; - let result = std::panic::catch_unwind(|| match self.expected_outcome { Outcome::Positive => { let queue = SimpleJobQueue::new(); @@ -170,9 +181,10 @@ impl Test { if let Err(e) = self.set_up_env(harness, context, async_result.clone()) { return (false, e); } + context.strict(strict); // TODO: timeout - let value = match context.eval(&*test_content) { + let value = match context.eval(source) { Ok(v) => v, Err(e) => return (false, format!("Uncaught {e}")), }; @@ -193,11 +205,12 @@ impl Test { error_type, ErrorType::SyntaxError, "non-SyntaxError parsing/early error found in {}", - self.name + self.path.display() ); let mut context = Context::default(); - match context.parse(&*test_content) { + context.strict(strict); + match context.parse(source) { Ok(statement_list) => match context.compile(&statement_list) { Ok(_) => (false, "StatementList compilation should fail".to_owned()), Err(e) => (true, format!("Uncaught {e:?}")), @@ -217,8 +230,9 @@ impl Test { if let Err(e) = self.set_up_env(harness, context, AsyncResult::default()) { return (false, e); } - let code = match Parser::new(test_content.as_bytes()) - .parse_all(context.interner_mut()) + context.strict(strict); + let code = match context + .parse(source) .map_err(Into::into) .and_then(|stmts| context.compile(&stmts)) { @@ -260,7 +274,7 @@ impl Test { let (result, result_text) = result.map_or_else( |_| { - eprintln!("last panic was on test \"{}\"", self.name); + eprintln!("last panic was on test \"{}\"", self.path.display()); (TestOutcomeResult::Panic, String::new()) }, |(res, text)| { @@ -275,7 +289,7 @@ impl Test { if verbose > 1 { println!( "`{}`{}: {}", - self.name, + self.path.display(), if strict { " (strict mode)" } else { "" }, if result == TestOutcomeResult::Passed { "Passed".green() @@ -299,7 +313,7 @@ impl Test { if verbose > 2 { println!( "`{}`{}: result text", - self.name, + self.path.display(), if strict { " (strict mode)" } else { "" }, ); println!("{result_text}"); @@ -331,29 +345,38 @@ impl Test { return Ok(()); } + let assert = Source::from_reader( + harness.assert.content.as_bytes(), + Some(&harness.assert.path), + ); + let sta = Source::from_reader(harness.sta.content.as_bytes(), Some(&harness.sta.path)); + context - .eval(harness.assert.as_ref()) + .eval(assert) .map_err(|e| format!("could not run assert.js:\n{e}"))?; context - .eval(harness.sta.as_ref()) + .eval(sta) .map_err(|e| format!("could not run sta.js:\n{e}"))?; if self.flags.contains(TestFlags::ASYNC) { + let dph = Source::from_reader( + harness.doneprint_handle.content.as_bytes(), + Some(&harness.doneprint_handle.path), + ); context - .eval(harness.doneprint_handle.as_ref()) + .eval(dph) .map_err(|e| format!("could not run doneprintHandle.js:\n{e}"))?; } - for include in self.includes.iter() { - context - .eval( - harness - .includes - .get(include) - .ok_or_else(|| format!("could not find the {include} include file."))? - .as_ref(), - ) - .map_err(|e| format!("could not run the {include} include file:\nUncaught {e}"))?; + for include_name in self.includes.iter() { + let include = harness + .includes + .get(include_name) + .ok_or_else(|| format!("could not find the {include_name} include file."))?; + let source = Source::from_reader(include.content.as_bytes(), Some(&include.path)); + context.eval(source).map_err(|e| { + format!("could not run the harness `{include_name}`:\nUncaught {e}",) + })?; } Ok(()) diff --git a/boa_tester/src/main.rs b/boa_tester/src/main.rs index c012b59cc8..e94c464080 100644 --- a/boa_tester/src/main.rs +++ b/boa_tester/src/main.rs @@ -297,16 +297,23 @@ fn run_test_suite( /// All the harness include files. #[derive(Debug, Clone)] struct Harness { - assert: Box, - sta: Box, - doneprint_handle: Box, - includes: FxHashMap, Box>, + assert: HarnessFile, + sta: HarnessFile, + doneprint_handle: HarnessFile, + includes: FxHashMap, HarnessFile>, +} + +#[derive(Debug, Clone)] +struct HarnessFile { + content: Box, + path: Box, } /// Represents a test suite. #[derive(Debug, Clone)] struct TestSuite { name: Box, + path: Box, suites: Box<[TestSuite]>, tests: Box<[Test]>, } @@ -362,7 +369,7 @@ enum TestOutcomeResult { } /// Represents a test. -#[derive(Debug, Clone, Default)] +#[derive(Debug, Clone)] #[allow(dead_code)] struct Test { name: Box, @@ -374,16 +381,16 @@ struct Test { expected_outcome: Outcome, includes: Box<[Box]>, locale: Locale, - content: Box, + path: Box, ignored: bool, } impl Test { /// Creates a new test. - fn new(name: N, content: C, metadata: MetaData) -> Self + fn new(name: N, path: C, metadata: MetaData) -> Self where N: Into>, - C: Into>, + C: Into>, { Self { name: name.into(), @@ -395,7 +402,7 @@ impl Test { expected_outcome: Outcome::from(metadata.negative), includes: metadata.includes, locale: metadata.locale, - content: content.into(), + path: path.into(), ignored: false, } } diff --git a/boa_tester/src/read.rs b/boa_tester/src/read.rs index 0d187c416e..685f260de2 100644 --- a/boa_tester/src/read.rs +++ b/boa_tester/src/read.rs @@ -1,6 +1,6 @@ //! Module to read the list of test suites from disk. -use crate::Ignored; +use crate::{HarnessFile, Ignored}; use super::{Harness, Locale, Phase, Test, TestSuite}; use color_eyre::{ @@ -9,7 +9,10 @@ use color_eyre::{ }; use fxhash::FxHashMap; use serde::Deserialize; -use std::{fs, io, path::Path}; +use std::{ + fs, io, + path::{Path, PathBuf}, +}; /// Representation of the YAML metadata in Test262 tests. #[derive(Debug, Clone, Deserialize)] @@ -84,6 +87,15 @@ pub(super) enum TestFlag { /// Reads the Test262 defined bindings. pub(super) fn read_harness(test262_path: &Path) -> Result { + fn read_harness_file(path: PathBuf) -> Result { + let content = fs::read_to_string(path.as_path()) + .wrap_err_with(|| format!("error reading the harness file `{}`", path.display()))?; + + Ok(HarnessFile { + content: content.into_boxed_str(), + path: path.into_boxed_path(), + }) + } let mut includes = FxHashMap::default(); for entry in fs::read_dir(test262_path.join("harness")) @@ -97,23 +109,14 @@ pub(super) fn read_harness(test262_path: &Path) -> Result { continue; } - let content = fs::read_to_string(entry.path()) - .wrap_err_with(|| format!("error reading the harnes/{file_name}"))?; - includes.insert( file_name.into_owned().into_boxed_str(), - content.into_boxed_str(), + read_harness_file(entry.path())?, ); } - let assert = fs::read_to_string(test262_path.join("harness/assert.js")) - .wrap_err("error reading harnes/assert.js")? - .into_boxed_str(); - let sta = fs::read_to_string(test262_path.join("harness/sta.js")) - .wrap_err("error reading harnes/sta.js")? - .into_boxed_str(); - let doneprint_handle = fs::read_to_string(test262_path.join("harness/doneprintHandle.js")) - .wrap_err("error reading harnes/doneprintHandle.js")? - .into_boxed_str(); + let assert = read_harness_file(test262_path.join("harness/assert.js"))?; + let sta = read_harness_file(test262_path.join("harness/sta.js"))?; + let doneprint_handle = read_harness_file(test262_path.join("harness/doneprintHandle.js"))?; Ok(Harness { assert, @@ -177,6 +180,7 @@ pub(super) fn read_suite( Ok(TestSuite { name: name.into(), + path: Box::from(path), suites: suites.into_boxed_slice(), tests: tests.into_boxed_slice(), }) @@ -200,16 +204,15 @@ pub(super) fn read_test(path: &Path) -> io::Result { ) })?; - let content = fs::read_to_string(path)?; - let metadata = read_metadata(&content, path)?; + let metadata = read_metadata(path)?; - Ok(Test::new(name, content, metadata)) + Ok(Test::new(name, path, metadata)) } /// Reads the metadata from the input test code. -fn read_metadata(code: &str, test: &Path) -> io::Result { +fn read_metadata(test: &Path) -> io::Result { use once_cell::sync::Lazy; - use regex::Regex; + use regex::bytes::Regex; /// Regular expression to retrieve the metadata of a test. static META_REGEX: Lazy = Lazy::new(|| { @@ -217,8 +220,10 @@ fn read_metadata(code: &str, test: &Path) -> io::Result { .expect("could not compile metadata regular expression") }); + let code = fs::read(test)?; + let yaml = META_REGEX - .captures(code) + .captures(&code) .ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidData, @@ -226,13 +231,13 @@ fn read_metadata(code: &str, test: &Path) -> io::Result { ) })? .get(1) + .map(|m| String::from_utf8_lossy(m.as_bytes())) .ok_or_else(|| { io::Error::new( io::ErrorKind::InvalidData, format!("no metadata found for test {}", test.display()), ) })? - .as_str() .replace('\r', "\n"); serde_yaml::from_str(&yaml).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e)) diff --git a/boa_wasm/src/lib.rs b/boa_wasm/src/lib.rs index 85fb45be99..13853f5c26 100644 --- a/boa_wasm/src/lib.rs +++ b/boa_wasm/src/lib.rs @@ -57,7 +57,7 @@ clippy::nursery, )] -use boa_engine::Context; +use boa_engine::{Context, Source}; use getrandom as _; use wasm_bindgen::prelude::*; @@ -66,7 +66,7 @@ use wasm_bindgen::prelude::*; pub fn evaluate(src: &str) -> Result { // Setup executor Context::default() - .eval(src) + .eval(Source::from_bytes(src)) .map_err(|e| JsValue::from(format!("Uncaught {e}"))) .map(|v| v.display().to_string()) }