Browse Source

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.
pull/2580/head
José Julián Espina 2 years ago
parent
commit
515d28f0a2
  1. 1
      Cargo.lock
  2. 21
      boa_cli/src/main.rs
  3. 8
      boa_engine/benches/full.rs
  4. 109
      boa_engine/src/builtins/array/tests.rs
  5. 4
      boa_engine/src/builtins/eval/mod.rs
  6. 7
      boa_engine/src/builtins/function/mod.rs
  7. 4
      boa_engine/src/builtins/json/mod.rs
  8. 12
      boa_engine/src/builtins/object/tests.rs
  9. 4
      boa_engine/src/builtins/promise/tests.rs
  10. 10
      boa_engine/src/builtins/weak/weak_ref.rs
  11. 4
      boa_engine/src/context/hooks.rs
  12. 47
      boa_engine/src/context/mod.rs
  13. 23
      boa_engine/src/lib.rs
  14. 6
      boa_engine/src/tests.rs
  15. 26
      boa_engine/src/value/serde_json.rs
  16. 18
      boa_engine/src/vm/tests.rs
  17. 6
      boa_examples/src/bin/classes.rs
  18. 12
      boa_examples/src/bin/closures.rs
  19. 9
      boa_examples/src/bin/commuter_visitor.rs
  20. 6
      boa_examples/src/bin/loadfile.rs
  21. 4
      boa_examples/src/bin/loadstring.rs
  22. 6
      boa_examples/src/bin/modulehandler.rs
  23. 9
      boa_examples/src/bin/symbol_visitor.rs
  24. 2
      boa_parser/src/lib.rs
  25. 71
      boa_parser/src/parser/mod.rs
  26. 5
      boa_parser/src/parser/tests/format/mod.rs
  27. 6
      boa_parser/src/parser/tests/mod.rs
  28. 88
      boa_parser/src/source.rs
  29. 1
      boa_tester/Cargo.toml
  30. 7
      boa_tester/src/exec/js262.rs
  31. 89
      boa_tester/src/exec/mod.rs
  32. 25
      boa_tester/src/main.rs
  33. 49
      boa_tester/src/read.rs
  34. 4
      boa_wasm/src/lib.rs

1
Cargo.lock generated

@ -323,7 +323,6 @@ dependencies = [
"bitflags",
"boa_engine",
"boa_gc",
"boa_parser",
"clap 4.1.4",
"color-eyre",
"colored",

21
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<S>(src: S, context: &mut Context<'_>) -> Result<StatementList, String>
fn parse_tokens<S>(src: &S, context: &mut Context<'_>) -> Result<StatementList, String>
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<S>(src: S, args: &Opt, context: &mut Context<'_>) -> Result<(), String>
fn dump<S>(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<FlowgraphDirection>,
) -> JsResult<String> {
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());
}

8
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(|| {

109
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();

4
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();
}

7
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,7 +512,8 @@ impl BuiltInFunctionObject {
parameters.push(u16::from(b')'));
// TODO: make parser generic to u32 iterators
let parameters = match Parser::new(String::from_utf16_lossy(&parameters).as_bytes())
let parameters =
match Parser::new(Source::from_bytes(&String::from_utf16_lossy(&parameters)))
.parse_formal_parameters(context.interner_mut(), generator, r#async)
{
Ok(parameters) => parameters,
@ -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,

4
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)?;

12
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]

4
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");

10
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()
);

4
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");
/// ```
///

47
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<S>(&mut self, src: S) -> JsResult<JsValue>
where
S: AsRef<[u8]>,
{
pub fn eval<R: Read>(&mut self, src: Source<'_, R>) -> JsResult<JsValue> {
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<S>(&mut self, src: S) -> Result<StatementList, ParseError>
where
S: AsRef<[u8]>,
{
/// Parse the given source script.
pub fn parse<R: Read>(&mut self, src: Source<'_, R>) -> Result<StatementList, ParseError> {
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());

23
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<T> = StdResult<T, JsError>;
@ -157,12 +160,12 @@ pub type JsResult<T> = StdResult<T, JsError>;
///
/// The state of the `Context` is changed, and a string representation of the result is returned.
#[cfg(test)]
pub(crate) fn forward<S>(context: &mut Context<'_>, src: S) -> String
pub(crate) fn forward<S>(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<T: AsRef<[u8]>>(context: &mut Context<'_>, src: T) -> JsResult<JsValue> {
pub(crate) fn forward_val<T: AsRef<[u8]> + ?Sized>(
context: &mut Context<'_>,
src: &T,
) -> JsResult<JsValue> {
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<T: AsRef<[u8]>>(context: &mut Context<'_>, src: T) ->
/// Create a clean Context and execute the code
#[cfg(test)]
pub(crate) fn exec<T: AsRef<[u8]>>(src: T) -> String {
let src_bytes: &[u8] = src.as_ref();
match Context::default().eval(src_bytes) {
pub(crate) fn exec<T: AsRef<[u8]> + ?Sized>(src: &T) -> String {
match Context::default().eval(Source::from_bytes(src)) {
Ok(value) => value.display().to_string(),
Err(error) => error.to_string(),
}

6
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);
}
}

26
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();

18
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")
);
}

6
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();
}

12
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()

9
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();

6
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();

4
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!(
"{}",

6
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<JsV
Ok(JsValue::Rational(-1.0))
} else {
// Load and parse the module source
ctx.eval(&buffer.unwrap()).unwrap();
ctx.eval(Source::from_bytes(&buffer.unwrap())).unwrap();
// Access module.exports and return as ResultValue
let global_obj = ctx.global_object().to_owned();

9
boa_examples/src/bin/symbol_visitor.rs

@ -3,11 +3,11 @@
// which mutates the AST.
use boa_ast::visitor::Visitor;
use boa_engine::Context;
use boa_engine::{Context, Source};
use boa_interner::Sym;
use boa_parser::Parser;
use core::ops::ControlFlow;
use std::{collections::HashSet, convert::Infallible, fs::File, io::BufReader};
use std::{collections::HashSet, convert::Infallible, path::Path};
#[derive(Debug, Clone, Default)]
struct SymbolVisitor {
@ -24,9 +24,8 @@ impl<'ast> 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();

2
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;

71
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,36 +105,21 @@ impl From<bool> 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<R> {
#[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<R>,
}
impl<R> Parser<R> {
/// 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),
}
}
/// Set the parser strict mode to true.
pub fn set_strict(&mut self)
where
R: Read,
{
self.cursor.set_strict_mode(true);
path: source.path,
cursor: Cursor::new(source.reader),
}
/// 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.
@ -145,10 +130,7 @@ impl<R> Parser<R> {
/// 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<StatementList>
where
R: Read,
{
pub fn parse_all(&mut self, interner: &mut Interner) -> ParseResult<StatementList> {
Script::new(false).parse(&mut self.cursor, interner)
}
@ -165,10 +147,7 @@ impl<R> Parser<R> {
&mut self,
direct: bool,
interner: &mut Interner,
) -> ParseResult<StatementList>
where
R: Read,
{
) -> ParseResult<StatementList> {
Script::new(direct).parse(&mut self.cursor, interner)
}
@ -184,10 +163,7 @@ impl<R> Parser<R> {
interner: &mut Interner,
allow_yield: bool,
allow_await: bool,
) -> ParseResult<StatementList>
where
R: Read,
{
) -> ParseResult<StatementList> {
FunctionStatementList::new(allow_yield, allow_await).parse(&mut self.cursor, interner)
}
@ -203,11 +179,26 @@ impl<R> Parser<R> {
interner: &mut Interner,
allow_yield: bool,
allow_await: bool,
) -> ParseResult<FormalParameterList>
) -> ParseResult<FormalParameterList> {
FormalParameters::new(allow_yield, allow_await).parse(&mut self.cursor, interner)
}
}
impl<R> 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);
}
}

5
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::<Vec<&'static str>>()
.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);

6
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<Box<[StatementListItem]>>,
{
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());
}

88
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],
/// <code>[Vec]<[u8]></code>, <code>[Box]<[\[u8\]][slice]></code> or a plain slice
/// <code>[&\[u8\]][slice]</code>.
///
/// # 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<T: AsRef<[u8]> + ?Sized>(source: &'bytes T) -> Self {
Self {
reader: source.as_ref(),
path: None,
}
}
}
impl<'path> Source<'path, BufReader<File>> {
/// 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<Self> {
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 }
}
}

1
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"

7
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<JsValue> {
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())),
},
)
}

89
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
for include_name in self.includes.iter() {
let include = 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}"))?;
.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(())

25
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<str>,
sta: Box<str>,
doneprint_handle: Box<str>,
includes: FxHashMap<Box<str>, Box<str>>,
assert: HarnessFile,
sta: HarnessFile,
doneprint_handle: HarnessFile,
includes: FxHashMap<Box<str>, HarnessFile>,
}
#[derive(Debug, Clone)]
struct HarnessFile {
content: Box<str>,
path: Box<Path>,
}
/// Represents a test suite.
#[derive(Debug, Clone)]
struct TestSuite {
name: Box<str>,
path: Box<Path>,
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<str>,
@ -374,16 +381,16 @@ struct Test {
expected_outcome: Outcome,
includes: Box<[Box<str>]>,
locale: Locale,
content: Box<str>,
path: Box<Path>,
ignored: bool,
}
impl Test {
/// Creates a new test.
fn new<N, C>(name: N, content: C, metadata: MetaData) -> Self
fn new<N, C>(name: N, path: C, metadata: MetaData) -> Self
where
N: Into<Box<str>>,
C: Into<Box<str>>,
C: Into<Box<Path>>,
{
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,
}
}

49
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<Harness> {
fn read_harness_file(path: PathBuf) -> Result<HarnessFile> {
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<Harness> {
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<Test> {
)
})?;
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<MetaData> {
fn read_metadata(test: &Path) -> io::Result<MetaData> {
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<Regex> = Lazy::new(|| {
@ -217,8 +220,10 @@ fn read_metadata(code: &str, test: &Path) -> io::Result<MetaData> {
.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<MetaData> {
)
})?
.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))

4
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<String, JsValue> {
// 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())
}

Loading…
Cancel
Save