diff --git a/nova_vm/src/ecmascript/builtins/ecmascript_function.rs b/nova_vm/src/ecmascript/builtins/ecmascript_function.rs index 24e9db622..cdfc48398 100644 --- a/nova_vm/src/ecmascript/builtins/ecmascript_function.rs +++ b/nova_vm/src/ecmascript/builtins/ecmascript_function.rs @@ -9,6 +9,7 @@ use core::{ use oxc_ast::ast::{FormalParameters, FunctionBody}; use oxc_ecmascript::IsSimpleParameterList; +use oxc_semantic::Semantic; use oxc_span::Span; use crate::{ @@ -26,7 +27,8 @@ use crate::{ }, scripts_and_modules::{ScriptOrModule, source_code::SourceCode}, syntax_directed_operations::function_definitions::{ - evaluate_async_function_body, evaluate_function_body, evaluate_generator_body, + CompileFunctionBodyData, evaluate_async_function_body, evaluate_function_body, + evaluate_generator_body, }, types::{ BUILTIN_STRING_MEMORY, ECMAScriptFunctionHeapData, Function, @@ -271,6 +273,39 @@ impl IndexMut> for Vec ECMAScriptFunction<'a> { + pub(crate) fn get_compile_data( + self, + agent: &Agent, + _gc: NoGcScope<'a, '_>, + ) -> CompileFunctionBodyData<'a> { + let ecmascript_function = &agent[self].ecmascript_function; + // SAFETY: We're alive so SourceCode must be too. + let (params, body) = unsafe { + ( + ecmascript_function.formal_parameters.as_ref(), + ecmascript_function.ecmascript_code.as_ref(), + ) + }; + CompileFunctionBodyData { + params, + body, + is_strict: ecmascript_function.strict, + is_lexical: ecmascript_function.this_mode == ThisMode::Lexical, + is_concise_body: ecmascript_function.is_concise_arrow_function, + } + } + + pub(crate) fn get_semantic( + self, + agent: &Agent, + gc: NoGcScope<'a, '_>, + ) -> &'a Semantic<'static> { + agent[self] + .ecmascript_function + .source_code + .get_semantic(agent, gc) + } + pub(crate) const fn _def() -> Self { ECMAScriptFunction(ECMAScriptFunctionIndex::from_u32_index(0)) } diff --git a/nova_vm/src/ecmascript/builtins/fundamental_objects/function_objects/function_constructor.rs b/nova_vm/src/ecmascript/builtins/fundamental_objects/function_objects/function_constructor.rs index 4e09f8aab..ba2608b95 100644 --- a/nova_vm/src/ecmascript/builtins/fundamental_objects/function_objects/function_constructor.rs +++ b/nova_vm/src/ecmascript/builtins/fundamental_objects/function_objects/function_constructor.rs @@ -2,6 +2,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. +use std::hint::unreachable_unchecked; + use oxc_span::SourceType; use crate::{ @@ -14,7 +16,7 @@ use crate::{ ordinary::get_prototype_from_constructor, ordinary_function_create, set_function_name, }, execution::{Agent, Environment, JsResult, ProtoIntrinsics, Realm, agent::ExceptionType}, - scripts_and_modules::source_code::{SourceCode, SourceCodeHeapData}, + scripts_and_modules::source_code::SourceCode, types::{ BUILTIN_STRING_MEMORY, Function, IntoObject, IntoValue, Object, Primitive, String, Value, @@ -108,14 +110,18 @@ impl DynamicFunctionKind { DynamicFunctionKind::AsyncGenerator => "async function*", } } - fn function_matches_kind(&self, function: &oxc_ast::ast::Function) -> bool { - let (is_async, is_generator) = match self { - DynamicFunctionKind::Normal => (false, false), - DynamicFunctionKind::Generator => (false, true), - DynamicFunctionKind::Async => (true, false), - DynamicFunctionKind::AsyncGenerator => (true, true), - }; - function.r#async == is_async && function.generator == is_generator + fn statement_matches_function_kind(&self, function: &oxc_ast::ast::Statement) -> bool { + if let oxc_ast::ast::Statement::FunctionDeclaration(function) = function { + let (is_async, is_generator) = match self { + DynamicFunctionKind::Normal => (false, false), + DynamicFunctionKind::Generator => (false, true), + DynamicFunctionKind::Async => (true, false), + DynamicFunctionKind::AsyncGenerator => (true, true), + }; + function.r#async == is_async && function.generator == is_generator + } else { + false + } } fn intrinsic_prototype(&self) -> ProtoIntrinsics { match self { @@ -234,75 +240,57 @@ pub(crate) fn create_dynamic_function<'a>( // avoid code injection, but oxc doesn't have a public API to do that. // Instead, we parse the source string as a script, and throw unless it has // exactly one statement which is a function declaration of the right kind. - let (function, source_code) = { - let mut function = None; - let mut source_code = None; - + let source_code = { let source_type = SourceType::default().with_script(true); - // SAFETY: The safety requirements are that the SourceCode cannot be - // GC'd before the program is dropped. If this function returns - // successfully, then the program's AST and the SourceCode will both be - // kept alive in the returned function object. - let parsed_result = - unsafe { SourceCode::parse_source(agent, source_string, source_type, gc.nogc()) }; - - if let Ok((program, sc)) = parsed_result { - source_code = Some(sc); - if program.hashbang.is_none() - && program.directives.is_empty() - && program.body.len() == 1 - { - if let oxc_ast::ast::Statement::FunctionDeclaration(funct) = &program.body[0] { - if kind.function_matches_kind(funct) { - // SAFETY: the Function is inside a oxc_allocator::Box, which will remain - // alive as long as `source_code` is kept alive. Similarly, the inner - // lifetime of Function is also kept alive by `source_code`.` - function = Some(unsafe { - core::mem::transmute::< - &oxc_ast::ast::Function, - &'static oxc_ast::ast::Function, - >(funct) - }); - } + match SourceCode::parse_source(agent, source_string, source_type, gc.nogc()) { + Ok(sc) => { + let program = sc.get_program(agent, gc.nogc()); + if !(program.hashbang.is_none() + && program.directives.is_empty() + && program.body.len() == 1 + && kind.statement_matches_function_kind(&program.body[0])) + { + return Err(agent.throw_exception_with_static_message( + ExceptionType::SyntaxError, + "Invalid function source text: Did not form a single function statement", + gc.into_nogc(), + )); } + sc } - } - - if let Some(function) = function { - (function, source_code.unwrap()) - } else { - if source_code.is_some() { - // In this branch, since we're not returning the function, we - // know `source_code` won't be reachable from any heap object, - // so we pop it off the heap to help GC along. - agent.heap.alloc_counter = agent - .heap - .alloc_counter - .saturating_sub(core::mem::size_of::>>()); - agent.heap.source_codes.pop(); - debug_assert_eq!( - source_code.unwrap().get_index(), - agent.heap.source_codes.len() - ); + Err(err) => { + let error_message = format!("Invalid function source text: {}", err[0].message); + return Err(agent.throw_exception( + ExceptionType::SyntaxError, + error_message, + gc.into_nogc(), + )); } - return Err(agent.throw_exception_with_static_message( - ExceptionType::SyntaxError, - "Invalid function source text.", - gc.into_nogc(), - )); } }; let source_code = source_code.scope(agent, gc.nogc()); + let function_prototype = get_prototype_from_constructor( + agent, + constructor.unbind(), + kind.intrinsic_prototype(), + gc.reborrow(), + ) + .unbind()?; + let gc = gc.into_nogc(); + let function_prototype = function_prototype.bind(gc); + let program = source_code.get(agent).get_program(agent, gc); + let Some(oxc_ast::ast::Statement::FunctionDeclaration(function)) = program.body.first() else { + // SAFETY: We checked that the function declaration matches our + // expectations before the getting the prototype from our constructor. + // That action can trigger GC, but we've scoped source_code so our + // Program hasn't been GC'd (this we know statically) but most + // importantly: there is no way that user code could have manipulated + // our AST, so our previous checks still apply. + unsafe { unreachable_unchecked() } + }; let params = OrdinaryFunctionCreateParams { - function_prototype: get_prototype_from_constructor( - agent, - constructor.unbind(), - kind.intrinsic_prototype(), - gc.reborrow(), - ) - .unbind()? - .bind(gc.nogc()), + function_prototype, // SAFETY: source_code was not shared. source_code: Some(unsafe { source_code.take(agent) }), source_text: function.span, @@ -318,13 +306,11 @@ pub(crate) fn create_dynamic_function<'a>( .global_env .unwrap() .unbind() - .bind(gc.nogc()), + .bind(gc), ), private_env: None, }; - let f = ordinary_function_create(agent, params, gc.nogc()).unbind(); - let gc = gc.into_nogc(); - let f = f.bind(gc); + let f = ordinary_function_create(agent, params, gc); set_function_name( agent, diff --git a/nova_vm/src/ecmascript/builtins/global_object.rs b/nova_vm/src/ecmascript/builtins/global_object.rs index 9090bcd4a..874874e0e 100644 --- a/nova_vm/src/ecmascript/builtins/global_object.rs +++ b/nova_vm/src/ecmascript/builtins/global_object.rs @@ -5,6 +5,7 @@ use ahash::AHashSet; use oxc_ast::ast::{BindingIdentifier, Program, VariableDeclarationKind}; use oxc_ecmascript::BoundNames; +use oxc_semantic::Semantic; use oxc_span::SourceType; use crate::ecmascript::abstract_operations::type_conversion::{ @@ -215,17 +216,10 @@ pub fn perform_eval<'gc>( } else { SourceType::default().with_script(true) }; - // SAFETY: Script is only kept alive for the duration of this call, and any - // references made to it by functions being created in the eval call will - // take a copy of the SourceCode. The SourceCode is also kept in the - // evaluation context and thus cannot be garbage collected while the eval - // call happens. - // The Program thus refers to a valid, live Allocator for the duration of - // this call. - let parse_result = unsafe { SourceCode::parse_source(agent, x, source_type, gc.nogc()) }; + let parse_result = SourceCode::parse_source(agent, x, source_type, gc.nogc()); // b. If script is a List of errors, throw a SyntaxError exception. - let Ok((script, source_code)) = parse_result else { + let Ok(source_code) = parse_result else { // TODO: Include error messages in the exception. return Err(agent.throw_exception_with_static_message( ExceptionType::SyntaxError, @@ -234,6 +228,9 @@ pub fn perform_eval<'gc>( )); }; + let script = source_code.get_program(agent, gc.nogc()); + let semantic = source_code.get_semantic(agent, gc.nogc()); + // c. If script Contains ScriptBody is false, return undefined. if script.is_empty() { return Ok(Value::Undefined); @@ -330,10 +327,21 @@ pub fn perform_eval<'gc>( // 27. Push evalContext onto the execution context stack; evalContext is now the running execution context. agent.push_execution_context(eval_context); + // SAFETY: We've pushed eval_context onto the execution context stack. The + // context holds our SourceCode and keeps it from being GC'd, meaning that + // the Program and Semantic will be kept alive for the duration of the + // eval_declaration_instantiation call. + let (script, semantic) = unsafe { + ( + core::mem::transmute::<&Program, &'static Program<'static>>(script), + core::mem::transmute::<&Semantic, &'static Semantic<'static>>(semantic), + ) + }; + // 28. Let result be Completion(EvalDeclarationInstantiation(body, varEnv, lexEnv, privateEnv, strictEval)). let result = eval_declaration_instantiation( agent, - &script, + script, ecmascript_code.variable_environment, ecmascript_code.lexical_environment, ecmascript_code.private_environment, @@ -346,8 +354,8 @@ pub fn perform_eval<'gc>( // 29. If result is a normal completion, then let result = match result { Ok(_) => { - let exe = - Executable::compile_eval_body(agent, &script, gc.nogc()).scope(agent, gc.nogc()); + let exe = Executable::compile_eval_body(agent, script, semantic, gc.nogc()) + .scope(agent, gc.nogc()); // a. Set result to Completion(Evaluation of body). // 30. If result is a normal completion and result.[[Value]] is empty, then // a. Set result to NormalCompletion(undefined). diff --git a/nova_vm/src/ecmascript/scripts_and_modules/script.rs b/nova_vm/src/ecmascript/scripts_and_modules/script.rs index 346943d68..6eb3fc67f 100644 --- a/nova_vm/src/ecmascript/scripts_and_modules/script.rs +++ b/nova_vm/src/ecmascript/scripts_and_modules/script.rs @@ -28,13 +28,13 @@ use ahash::AHashSet; use core::{ any::Any, marker::PhantomData, - mem::ManuallyDrop, ops::{Index, IndexMut}, }; use oxc_ast::ast::{BindingIdentifier, Program, VariableDeclarationKind}; use oxc_diagnostics::OxcDiagnostic; use oxc_ecmascript::BoundNames; use oxc_span::SourceType; +use std::ptr::NonNull; use super::source_code::SourceCode; @@ -53,7 +53,11 @@ impl core::fmt::Debug for Script<'_> { } } -impl Script<'_> { +impl<'a> Script<'a> { + pub(crate) fn get_source_code(self, agent: &Agent) -> SourceCode<'a> { + agent[self].source_code + } + /// Creates a script identififer from a usize. /// /// ## Panics @@ -147,7 +151,7 @@ pub struct ScriptRecord<'a> { /// allocator drops all of the data in a single go. All that needs to be /// dropped here is the local Program itself, not any of its referred /// parts. - pub(crate) ecmascript_code: ManuallyDrop>, + pub(crate) ecmascript_code: NonNull>, /// ### \[\[LoadedModules]] /// @@ -281,9 +285,9 @@ pub fn parse_script<'a>( // SAFETY: Script keeps the SourceCode reference alive in the Heap, thus // making the Program's references point to a live Allocator. - let parse_result = unsafe { SourceCode::parse_source(agent, source_text, source_type, gc) }; + let parse_result = SourceCode::parse_source(agent, source_text, source_type, gc); - let (program, source_code) = match parse_result { + let source_code = match parse_result { // 2. If script is a List of errors, return script. Ok(result) => result, Err(errors) => { @@ -296,7 +300,7 @@ pub fn parse_script<'a>( // [[Realm]]: realm, realm: realm.unbind(), // [[ECMAScriptCode]]: script, - ecmascript_code: ManuallyDrop::new(program), + ecmascript_code: source_code.get_program_pointer(agent), // [[LoadedModules]]: « », loaded_modules: (), // [[HostDefined]]: hostDefined, @@ -321,7 +325,15 @@ pub fn script_evaluation<'a>( let script = script.bind(gc.nogc()); let script_record = &agent[script]; let realm_id = script_record.realm; - let is_strict_mode = script_record.ecmascript_code.source_type.is_strict(); + // SAFETY: Script is currently alive, meaning that the SourceCode is + // currently alive, and thus the Program pointer is still valid. + let is_strict_mode = unsafe { + script_record + .ecmascript_code + .as_ref() + .source_type + .is_strict() + }; let source_code = script_record.source_code; let realm = agent.get_realm_record_by_id(realm_id); @@ -442,7 +454,7 @@ pub(crate) fn global_declaration_instantiation<'a>( // long as the Script is alive in the heap as they are not reallocated. // Thus in effect VarScopedDeclaration<'_> is valid for the duration // of the global_declaration_instantiation call. - let script = unsafe { core::mem::transmute::<&Program, &'static Program<'static>>(script) }; + let script = unsafe { script.as_ref() }; // 1. Let lexNames be the LexicallyDeclaredNames of script. let lex_names = script_lexically_declared_names(script); // 2. Let varNames be the VarDeclaredNames of script. diff --git a/nova_vm/src/ecmascript/scripts_and_modules/source_code.rs b/nova_vm/src/ecmascript/scripts_and_modules/source_code.rs index 8787a85b0..c7d3f1566 100644 --- a/nova_vm/src/ecmascript/scripts_and_modules/source_code.rs +++ b/nova_vm/src/ecmascript/scripts_and_modules/source_code.rs @@ -8,12 +8,13 @@ //! SourceCode for their function source text. use core::{fmt::Debug, ops::Index, ptr::NonNull}; +use std::ops::IndexMut; use oxc_allocator::Allocator; use oxc_ast::ast::Program; use oxc_diagnostics::OxcDiagnostic; use oxc_parser::{Parser, ParserReturn}; -use oxc_semantic::{SemanticBuilder, SemanticBuilderReturn}; +use oxc_semantic::{Semantic, SemanticBuilder, SemanticBuilderReturn}; use oxc_span::SourceType; use crate::{ @@ -42,19 +43,14 @@ impl core::fmt::Debug for SourceCode<'_> { } impl<'a> SourceCode<'a> { - /// Parses the given source string as JavaScript code and returns the - /// parsed result and a SourceCode heap reference. - /// - /// ### Safety - /// - /// The caller must keep the SourceCode from being garbage collected until - /// they drop the parsed code. - pub(crate) unsafe fn parse_source( + /// Parses the given source string as JavaScript code and returns a + /// SourceCode heap reference. + pub(crate) fn parse_source( agent: &mut Agent, source: String, source_type: SourceType, gc: NoGcScope<'a, '_>, - ) -> Result<(Program<'static>, Self), Vec> { + ) -> Result> { // If the source code is not a heap string, pad it with whitespace and // allocate it on the heap. This makes it safe (for some definition of // "safe") for the any functions created referring to this source code to @@ -109,35 +105,87 @@ impl<'a> SourceCode<'a> { return Err(errors); } - let SemanticBuilderReturn { errors, .. } = SemanticBuilder::new() + // SAFETY: We promise to drop the Allocator only after Program has been + // dropped, so the Program can consider its internal references as + // 'static. + let mut program = unsafe { + core::mem::transmute::, NonNull>>(NonNull::from( + Box::leak(Box::new(program)), + )) + }; + + let SemanticBuilderReturn { errors, semantic } = SemanticBuilder::new() .with_check_syntax_error(true) - .build(&program); + // SAFETY: program is alive and well right now, and we promise to + // drop semantic before program. + .build(unsafe { program.as_ref() }); if !errors.is_empty() { - // Drop program before dropping allocator. + // Drop semantic & program before dropping allocator. #[allow(clippy::drop_non_drop)] - drop(program); + drop(semantic); + #[allow(clippy::drop_non_drop)] + // SAFETY: No references to program exist anymore. It is safe to + // drop. + drop(unsafe { Box::from_raw(program.as_mut()) }); // SAFETY: No references to allocator exist anymore. It is safe to - // drop it. + // drop. drop(unsafe { Box::from_raw(allocator.as_mut()) }); // TODO: Include error messages in the exception. return Err(errors); } - // SAFETY: Caller guarantees that they will drop the Program before - // SourceCode can be garbage collected. - let program = unsafe { core::mem::transmute::>(program) }; - let source_code = agent.heap.create(SourceCodeHeapData { - source: source.unbind(), - allocator, - }); + // SAFETY: We promise to drop the Semantic before Program and + // Allocator, so the Semantic can consider its internal references as + // 'static. + let semantic = unsafe { + core::mem::transmute::, NonNull>>(NonNull::from( + Box::leak(Box::new(semantic)), + )) + }; + let source_code = agent.heap.create(SourceCodeHeapData::new( + source, program, semantic, allocator, + )); - Ok((program, source_code)) + Ok(source_code) } pub(crate) fn get_source_text(self, agent: &Agent) -> &str { agent[agent[self].source].as_str() } + /// Get a reference to the program AST of this SourceCode. + /// + /// ## Safety + /// + /// The program AST is valid until the SourceCode is garbage collected. + pub(crate) fn get_program(self, agent: &Agent, gc: NoGcScope<'a, '_>) -> &'a Program<'a> { + agent[self].get_program(gc) + } + + /// Get a non-null pointer to the program AST of this SourceCode. + /// + /// ## Safety + /// + /// The program AST pointer is valid until the SourceCode is garbage + /// collected. + pub(crate) fn get_program_pointer(self, agent: &Agent) -> NonNull> { + agent[self].program + } + + /// Get a reference to the semantic analysis results of this SourceCode. + /// + /// ## Safety + /// + /// The semantic analysis results are valid until the SourceCode is + /// garbage collected. + pub(crate) fn get_semantic( + self, + agent: &Agent, + gc: NoGcScope<'a, '_>, + ) -> &'a Semantic<'static> { + agent[self].get_semantic(gc) + } + pub(crate) fn get_index(self) -> usize { self.0.into_index() } @@ -150,9 +198,67 @@ pub struct SourceCodeHeapData<'a> { /// string was small-string optimised and on the stack, then those /// references would necessarily and definitely be invalid. source: HeapString<'a>, - /// The arena that contains the parsed data of the eval source. + /// The semantic analysis results of the source code. + /// + /// ## Safety + /// + /// The semantic analysis results contains self-referential pointers into + /// the program and allocator fields. It must be dropped before the others. + semantic: NonNull>, + /// The parsed AST of the source code. + /// + /// ## Safety + /// + /// The program contains self-referential pointers into the allocator + /// field. It must be dropped before the allocator. + program: NonNull>, + /// The arena allocator that contains the parsed data of the eval source. allocator: NonNull, } +impl<'a> SourceCodeHeapData<'a> { + fn new( + source: HeapString<'a>, + program: NonNull>, + semantic: NonNull>, + allocator: NonNull, + ) -> Self { + Self { + source, + semantic, + program, + allocator, + } + } + + /// Get a reference to the program AST of this SourceCode. + /// + /// ## Safety + /// + /// The program AST is valid until the SourceCode is garbage collected. + pub(crate) fn get_program( + &self, + // NoGcScope used only as proof. + _gc: NoGcScope<'a, '_>, + ) -> &'a Program<'a> { + unsafe { core::mem::transmute::<&Program, &'a Program<'a>>(self.program.as_ref()) } + } + + /// Get a reference to the semantic analysis results of this SourceCode. + /// + /// ## Safety + /// + /// The semantic analysis results are valid until the SourceCode is + /// garbage collected. + fn get_semantic( + &self, + // NoGcScope used only as proof. + _gc: NoGcScope<'a, '_>, + ) -> &'a Semantic<'static> { + // SAFETY: SourceCodeHeapData only drops Semantic, Program, and + // Allocator when it is dropped, ie. GC'd. + unsafe { core::mem::transmute::<&Semantic, &'a Semantic<'static>>(self.semantic.as_ref()) } + } +} unsafe impl Send for SourceCodeHeapData<'_> {} @@ -169,7 +275,13 @@ impl Drop for SourceCodeHeapData<'_> { fn drop(&mut self) { // SAFETY: All references to this SourceCode should have been dropped // before we drop this. - drop(unsafe { Box::from_raw(self.allocator.as_mut()) }); + unsafe { + // Note: The drop order here is important. Semantic refers to + // Program and Allocator, Program refers to Allocator. + drop(Box::from_raw(self.semantic.as_mut())); + drop(Box::from_raw(self.program.as_mut())); + drop(Box::from_raw(self.allocator.as_mut())); + } } } @@ -185,6 +297,16 @@ impl Index> for Agent { .expect("SourceCode slot empty") } } +impl IndexMut> for Agent { + fn index_mut(&mut self, index: SourceCode<'_>) -> &mut Self::Output { + self.heap + .source_codes + .get_mut(index.get_index()) + .expect("SourceCode out of bounds") + .as_mut() + .expect("SourceCode slot empty") + } +} // SAFETY: Property implemented as a lifetime transmute. unsafe impl Bindable for SourceCode<'_> { @@ -249,18 +371,12 @@ unsafe impl Bindable for SourceCodeHeapData<'_> { impl HeapMarkAndSweep for SourceCodeHeapData<'static> { fn mark_values(&self, queues: &mut WorkQueues) { - let Self { - source, - allocator: _, - } = self; + let Self { source, .. } = self; source.mark_values(queues); } fn sweep_values(&mut self, compactions: &CompactionLists) { - let Self { - source, - allocator: _, - } = self; + let Self { source, .. } = self; source.sweep_values(compactions); } } diff --git a/nova_vm/src/ecmascript/syntax_directed_operations/function_definitions.rs b/nova_vm/src/ecmascript/syntax_directed_operations/function_definitions.rs index b0eea112d..60b37c76b 100644 --- a/nova_vm/src/ecmascript/syntax_directed_operations/function_definitions.rs +++ b/nova_vm/src/ecmascript/syntax_directed_operations/function_definitions.rs @@ -13,7 +13,7 @@ use crate::engine::unwrap_try; use crate::{ ecmascript::{ builtins::{ - ArgumentsList, ECMAScriptFunction, OrdinaryFunctionCreateParams, ThisMode, + ArgumentsList, ECMAScriptFunction, OrdinaryFunctionCreateParams, control_abstraction_objects::{ async_function_objects::await_reaction::AwaitReaction, generator_objects::GeneratorState, @@ -244,33 +244,13 @@ pub(crate) fn instantiate_ordinary_function_expression<'a>( } pub(crate) struct CompileFunctionBodyData<'a> { - pub(crate) params: &'a oxc_ast::ast::FormalParameters<'static>, - pub(crate) body: &'a oxc_ast::ast::FunctionBody<'static>, + pub(crate) params: &'a oxc_ast::ast::FormalParameters<'a>, + pub(crate) body: &'a oxc_ast::ast::FunctionBody<'a>, pub(crate) is_strict: bool, pub(crate) is_lexical: bool, pub(crate) is_concise_body: bool, } -impl CompileFunctionBodyData<'static> { - fn new(agent: &mut Agent, function: ECMAScriptFunction) -> Self { - let ecmascript_function = &agent[function].ecmascript_function; - // SAFETY: We're alive so SourceCode must be too. - let (params, body) = unsafe { - ( - ecmascript_function.formal_parameters.as_ref(), - ecmascript_function.ecmascript_code.as_ref(), - ) - }; - CompileFunctionBodyData { - params, - body, - is_strict: ecmascript_function.strict, - is_lexical: ecmascript_function.this_mode == ThisMode::Lexical, - is_concise_body: ecmascript_function.is_concise_arrow_function, - } - } -} - /// ### [15.2.3 Runtime Semantics: EvaluateFunctionBody](https://tc39.es/ecma262/#sec-runtime-semantics-evaluatefunctionbody) /// The syntax-directed operation EvaluateFunctionBody takes arguments /// functionObject (an ECMAScript function object) and argumentsList (a List of @@ -290,8 +270,7 @@ pub(crate) fn evaluate_function_body<'gc>( let exe = if let Some(exe) = agent[function_object].compiled_bytecode { exe.bind(gc.nogc()) } else { - let data = CompileFunctionBodyData::new(agent, function_object); - let exe = Executable::compile_function_body(agent, data, gc.nogc()); + let exe = Executable::compile_function_body(agent, function_object, gc.nogc()); agent[function_object].compiled_bytecode = Some(exe.unbind()); exe }; @@ -326,8 +305,7 @@ pub(crate) fn evaluate_async_function_body<'a>( let exe = if let Some(exe) = agent[function_object].compiled_bytecode { exe.bind(gc.nogc()) } else { - let data = CompileFunctionBodyData::new(agent, function_object); - let exe = Executable::compile_function_body(agent, data, gc.nogc()); + let exe = Executable::compile_function_body(agent, function_object, gc.nogc()); agent[function_object].compiled_bytecode = Some(exe.unbind()); exe }; @@ -458,8 +436,8 @@ pub(crate) fn evaluate_generator_body<'gc>( // 4. Perform GeneratorStart(G, FunctionBody). // SAFETY: We're alive so SourceCode must be too. - let data = CompileFunctionBodyData::new(agent, scoped_function_object.get(agent)); - let executable = Executable::compile_function_body(agent, data, gc); + let executable = + Executable::compile_function_body(agent, scoped_function_object.get(agent), gc); agent[generator].generator_state = Some(GeneratorState::Suspended(SuspendedGeneratorState { vm_or_args: VmOrArguments::Arguments( arguments_list @@ -535,8 +513,7 @@ pub(crate) fn evaluate_async_generator_body<'gc>( let executable = if let Some(exe) = agent[function_object].compiled_bytecode { exe.bind(gc) } else { - let data = CompileFunctionBodyData::new(agent, function_object); - let exe = Executable::compile_function_body(agent, data, gc); + let exe = Executable::compile_function_body(agent, function_object, gc); agent[function_object].compiled_bytecode = Some(exe.unbind()); exe }; diff --git a/nova_vm/src/engine/bytecode/bytecode_compiler/class_definition_evaluation.rs b/nova_vm/src/engine/bytecode/bytecode_compiler/class_definition_evaluation.rs index e28e2df41..797ccd2ae 100644 --- a/nova_vm/src/engine/bytecode/bytecode_compiler/class_definition_evaluation.rs +++ b/nova_vm/src/engine/bytecode/bytecode_compiler/class_definition_evaluation.rs @@ -438,7 +438,7 @@ impl<'s> CompileEvaluation<'s> for ast::Class<'s> { // 28. Set F.[[PrivateMethods]] to instancePrivateMethods. // 29. Set F.[[Fields]] to instanceFields. if !instance_fields.is_empty() { - let mut constructor_ctx = CompileContext::new(ctx.agent, ctx.gc); + let mut constructor_ctx = CompileContext::new(ctx.agent, ctx.semantic, ctx.gc); for ele in instance_fields { match ele { PropertyInitializerField::Static((property_key, value)) => { diff --git a/nova_vm/src/engine/bytecode/bytecode_compiler/compile_context.rs b/nova_vm/src/engine/bytecode/bytecode_compiler/compile_context.rs index 4e7ece97d..e6ea1d4c5 100644 --- a/nova_vm/src/engine/bytecode/bytecode_compiler/compile_context.rs +++ b/nova_vm/src/engine/bytecode/bytecode_compiler/compile_context.rs @@ -2,8 +2,8 @@ // License, v. 2.0. If a copy of the MPL was not distributed with this // file, You can obtain one at https://mozilla.org/MPL/2.0/. -use oxc_ast::ast::{self, LabelIdentifier, Statement}; -use oxc_span::Atom; +use oxc_ast::ast; +use oxc_semantic::Semantic; use crate::{ ecmascript::{ @@ -78,6 +78,7 @@ pub(crate) struct JumpIndex { /// tracks it. pub(crate) struct CompileContext<'agent, 'script, 'gc, 'scope> { pub(crate) agent: &'agent mut Agent, + pub(crate) semantic: &'gc Semantic<'static>, pub(crate) gc: NoGcScope<'gc, 'scope>, /// true if the current last instruction is a terminal instruction and no /// jumps point past it. @@ -109,10 +110,12 @@ pub(crate) struct CompileContext<'agent, 'script, 'gc, 'scope> { impl<'a, 's, 'gc, 'scope> CompileContext<'a, 's, 'gc, 'scope> { pub(crate) fn new( agent: &'a mut Agent, + semantic: &'gc Semantic<'static>, gc: NoGcScope<'gc, 'scope>, ) -> CompileContext<'a, 's, 'gc, 'scope> { CompileContext { agent, + semantic, gc, // Note: when no instructions exist, we are indeed terminal. last_instruction_is_terminal: true, @@ -130,7 +133,7 @@ impl<'a, 's, 'gc, 'scope> CompileContext<'a, 's, 'gc, 'scope> { } /// Enter a labelled statement. - pub(super) fn enter_label(&mut self, label: &'s LabelIdentifier<'s>) { + pub(super) fn enter_label(&mut self, label: &'s ast::LabelIdentifier<'s>) { self.control_flow_stack .push(ControlFlowStackEntry::LabelledStatement { label, @@ -345,7 +348,7 @@ impl<'a, 's, 'gc, 'scope> CompileContext<'a, 's, 'gc, 'scope> { } /// Enter a for, for-in, or while loop. - pub(super) fn enter_loop(&mut self, label_set: Option>>) { + pub(super) fn enter_loop(&mut self, label_set: Option>>) { self.control_flow_stack.push(ControlFlowStackEntry::Loop { label_set, incoming_control_flows: None, @@ -368,7 +371,7 @@ impl<'a, 's, 'gc, 'scope> CompileContext<'a, 's, 'gc, 'scope> { } /// Enter a switch block. - pub(super) fn enter_switch(&mut self, label_set: Option>>) { + pub(super) fn enter_switch(&mut self, label_set: Option>>) { self.control_flow_stack.push(ControlFlowStackEntry::Switch { label_set, incoming_control_flows: None, @@ -393,7 +396,7 @@ impl<'a, 's, 'gc, 'scope> CompileContext<'a, 's, 'gc, 'scope> { /// Enter a for-of loop. pub(super) fn enter_iterator( &mut self, - label_set: Option>>, + label_set: Option>>, ) -> JumpIndex { self.control_flow_stack .push(ControlFlowStackEntry::Iterator { @@ -427,7 +430,7 @@ impl<'a, 's, 'gc, 'scope> CompileContext<'a, 's, 'gc, 'scope> { /// Enter a for-await-of loop. pub(super) fn enter_async_iterator( &mut self, - label_set: Option>>, + label_set: Option>>, ) -> JumpIndex { self.control_flow_stack .push(ControlFlowStackEntry::AsyncIterator { @@ -468,7 +471,7 @@ impl<'a, 's, 'gc, 'scope> CompileContext<'a, 's, 'gc, 'scope> { /// site before jumping to the target. If user-defined finally-blocks are /// present in the finaliser stack, the method instead jumps to a /// finally-block that ends with a jump to the final target. - pub(super) fn compile_break(&mut self, label: Option<&'s LabelIdentifier<'s>>) { + pub(super) fn compile_break(&mut self, label: Option<&'s ast::LabelIdentifier<'s>>) { for entry in self.control_flow_stack.iter_mut().rev() { if entry.is_break_target_for(label) { // Stop iterating the stack when we find our target and push @@ -502,7 +505,7 @@ impl<'a, 's, 'gc, 'scope> CompileContext<'a, 's, 'gc, 'scope> { /// site before jumping to the target. If user-defined finally-blocks are /// present in the finaliser stack, the method instead jumps to a /// finally-block that ends with a jump to the final target. - pub(super) fn compile_continue(&mut self, label: Option<&'s LabelIdentifier<'s>>) { + pub(super) fn compile_continue(&mut self, label: Option<&'s ast::LabelIdentifier<'s>>) { for entry in self.control_flow_stack.iter_mut().rev() { if entry.is_continue_target_for(label) { // Stop iterating the stack when we find our target and push @@ -696,12 +699,13 @@ impl<'a, 's, 'gc, 'scope> CompileContext<'a, 's, 'gc, 'scope> { // SAFETY: Script referred by the Function uniquely owns the Program // and the body buffer does not move under any circumstances during // heap operations. - let body: &[Statement] = unsafe { core::mem::transmute(data.body.statements.as_slice()) }; + let body: &[ast::Statement] = + unsafe { core::mem::transmute(data.body.statements.as_slice()) }; self.compile_statements(body); } - pub(crate) fn compile_statements(&mut self, body: &'s [Statement<'s>]) { + pub(crate) fn compile_statements(&mut self, body: &'s [ast::Statement<'s>]) { let iter = body.iter(); for stmt in iter { @@ -729,7 +733,7 @@ impl<'a, 's, 'gc, 'scope> CompileContext<'a, 's, 'gc, 'scope> { }) } - pub(crate) fn create_identifier(&mut self, atom: &Atom<'_>) -> String<'gc> { + pub(crate) fn create_identifier(&mut self, atom: &oxc_span::Atom<'_>) -> String<'gc> { let existing = self.constants.iter().find_map(|constant| { if let Ok(existing_identifier) = String::try_from(*constant) { if existing_identifier.as_str(self.agent) == atom.as_str() { @@ -998,7 +1002,7 @@ pub(crate) trait CompileEvaluation<'s> { pub(crate) trait CompileLabelledEvaluation<'s> { fn compile_labelled( &'s self, - label_set: Option<&mut Vec<&'s LabelIdentifier<'s>>>, + label_set: Option<&mut Vec<&'s ast::LabelIdentifier<'s>>>, ctx: &mut CompileContext<'_, 's, '_, '_>, ); } diff --git a/nova_vm/src/engine/bytecode/bytecode_compiler/function_declaration_instantiation.rs b/nova_vm/src/engine/bytecode/bytecode_compiler/function_declaration_instantiation.rs index 4c8f6a51e..6fefaa34b 100644 --- a/nova_vm/src/engine/bytecode/bytecode_compiler/function_declaration_instantiation.rs +++ b/nova_vm/src/engine/bytecode/bytecode_compiler/function_declaration_instantiation.rs @@ -5,7 +5,9 @@ use ahash::{AHashMap, AHashSet}; use oxc_ast::ast::{FormalParameters, FunctionBody}; use oxc_ecmascript::BoundNames; +use oxc_semantic::Semantic; use oxc_span::Atom; +use oxc_span::GetSpan; use crate::{ ecmascript::{ @@ -24,6 +26,36 @@ use crate::{ use super::{CompileEvaluation, complex_array_pattern, simple_array_pattern}; +/// Check if there is a global/unresolved reference in some part of the +/// source code being compiled. +fn function_has_unresolved_arguments_decl( + sema: &Semantic, + arguments_span: oxc_span::Span, + body_span: oxc_span::Span, +) -> bool { + if arguments_span.is_empty() && body_span.is_empty() { + return false; + } + + let scoping = sema.scoping(); + let nodes = sema.nodes(); + + let Some(reference_ids) = scoping.root_unresolved_references().get("arguments") else { + // No unresolved "arguments" references in the entire script. + return false; + }; + + for reference_id in reference_ids.iter().copied() { + let reference = scoping.get_reference(reference_id); + let span = nodes.get_node(reference.node_id()).span(); + if arguments_span.contains_inclusive(span) || body_span.contains_inclusive(span) { + return true; + } + } + + false +} + pub(crate) fn instantiation<'s>( ctx: &mut CompileContext<'_, 's, '_, '_>, formals: &'s FormalParameters<'s>, @@ -67,21 +99,33 @@ pub(crate) fn instantiation<'s>( } } - // 15. Let argumentsObjectNeeded be true. - // 16. If func.[[ThisMode]] is lexical, then - // a. NOTE: Arrow functions never have an arguments object. - // b. Set argumentsObjectNeeded to false. - // 17. Else if parameterNames contains "arguments", then - // a. Set argumentsObjectNeeded to false. // 18. Else if hasParameterExpressions is false, then - // a. If functionNames contains "arguments" or lexicalNames contains "arguments", then - // i. Set argumentsObjectNeeded to false. - let arguments_object_needed = !is_lexical - && !parameter_names.contains("arguments") - && (has_parameter_expressions - || (!functions.contains_key("arguments") - && !function_body_lexically_declared_names(body) - .contains(&Atom::from("arguments")))); + + // 15. Let argumentsObjectNeeded be true. + let arguments_object_needed = if is_lexical { + // 16. If func.[[ThisMode]] is lexical, then + // a. NOTE: Arrow functions never have an arguments object. + // b. Set argumentsObjectNeeded to true. + false + } else if parameter_names.contains("arguments") { + // 17. Else if parameterNames contains "arguments", then + // a. Set argumentsObjectNeeded to true. + false + } else if !has_parameter_expressions { + // 18. Else if hasParameterExpressions is false, then + if functions.contains_key("arguments") + || function_body_lexically_declared_names(body).contains(&Atom::from("arguments")) + { + false + } else { + function_has_unresolved_arguments_decl(ctx.semantic, formals.span, body.span) + } + } else { + // OPTIMISATION: if the body does not contain a "free-standing" + // reference to the "arguments" name then we don't need to create + // an object for it. + function_has_unresolved_arguments_decl(ctx.semantic, formals.span, body.span) + }; // 19. If strict is true or hasParameterExpressions is false, then // a. NOTE: Only a single Environment Record is needed for the parameters, since calls to eval in strict mode code cannot create new bindings which are visible outside of the eval. diff --git a/nova_vm/src/engine/bytecode/executable.rs b/nova_vm/src/engine/bytecode/executable.rs index 8a640674b..9bdc47f17 100644 --- a/nova_vm/src/engine/bytecode/executable.rs +++ b/nova_vm/src/engine/bytecode/executable.rs @@ -10,9 +10,9 @@ use std::marker::PhantomData; use crate::{ ecmascript::{ + builtins::ECMAScriptFunction, execution::Agent, scripts_and_modules::script::Script, - syntax_directed_operations::function_definitions::CompileFunctionBodyData, types::{String, Value}, }, engine::{ @@ -25,7 +25,8 @@ use crate::{ }, heap::{CompactionLists, CreateHeapData, Heap, HeapMarkAndSweep, WorkQueues}, }; -use oxc_ast::ast::{self, Program, Statement}; +use oxc_ast::ast::{self, Program}; +use oxc_semantic::Semantic; #[derive(Debug)] /// A `Send` and `Sync` wrapper over a `&'static T` where `T` might not itself @@ -148,23 +149,25 @@ impl<'gc> Executable<'gc> { eprintln!("=== Compiling Script ==="); eprintln!(); } - // SAFETY: Script uniquely owns the Program and the body buffer does - // not move under any circumstances during heap operations. - let body: &[Statement] = - unsafe { core::mem::transmute(agent[script].ecmascript_code.body.as_slice()) }; - let mut ctx = CompileContext::new(agent, gc); + let script = script.bind(gc); + let source_code = script.get_source_code(agent); + let program = source_code.get_program(agent, gc); + let semantic = source_code.get_semantic(agent, gc); + let mut ctx = CompileContext::new(agent, semantic, gc); - ctx.compile_statements(body); + ctx.compile_statements(&program.body); ctx.do_implicit_return(); ctx.finish() } pub(crate) fn compile_function_body( agent: &mut Agent, - data: CompileFunctionBodyData<'_>, + function: ECMAScriptFunction, gc: NoGcScope<'gc, '_>, ) -> Self { - let mut ctx = CompileContext::new(agent, gc); + let function = function.bind(gc); + let data = function.get_compile_data(agent, gc); + let mut ctx = CompileContext::new(agent, function.get_semantic(agent, gc), gc); let is_concise = data.is_concise_body; @@ -179,7 +182,8 @@ impl<'gc> Executable<'gc> { pub(crate) fn compile_eval_body( agent: &mut Agent, - program: &Program, + program: &'gc Program<'static>, + semantic: &'gc Semantic<'static>, gc: NoGcScope<'gc, '_>, ) -> Self { if agent.options.print_internals { @@ -187,7 +191,7 @@ impl<'gc> Executable<'gc> { eprintln!("=== Compiling Eval Body ==="); eprintln!(); } - let mut ctx = CompileContext::new(agent, gc); + let mut ctx = CompileContext::new(agent, semantic, gc); // eval('"asd"') is parsed into an empty body with a single directive. // Multiple directives are also possible, but only the last one is diff --git a/tests/expectations.json b/tests/expectations.json index 0bcda3e29..18da61d4b 100644 --- a/tests/expectations.json +++ b/tests/expectations.json @@ -9346,6 +9346,8 @@ "language/arguments-object/10.6-13-a-1.js": "FAIL", "language/arguments-object/10.6-13-a-2.js": "FAIL", "language/arguments-object/10.6-13-a-3.js": "FAIL", + "language/arguments-object/10.6-6-3.js": "FAIL", + "language/arguments-object/10.6-6-4.js": "FAIL", "language/arguments-object/S10.6_A3_T3.js": "FAIL", "language/arguments-object/S10.6_A3_T4.js": "FAIL", "language/arguments-object/S10.6_A4.js": "FAIL", @@ -9487,6 +9489,22 @@ "language/eval-code/direct/async-gen-named-func-expr-fn-body-cntns-arguments-var-bind-declare-arguments.js": "FAIL", "language/eval-code/direct/async-gen-named-func-expr-no-pre-existing-arguments-bindings-are-present-declare-arguments-and-assign.js": "FAIL", "language/eval-code/direct/async-gen-named-func-expr-no-pre-existing-arguments-bindings-are-present-declare-arguments.js": "FAIL", + "language/eval-code/direct/func-decl-fn-body-cntns-arguments-func-decl-declare-arguments-and-assign.js": "FAIL", + "language/eval-code/direct/func-decl-fn-body-cntns-arguments-func-decl-declare-arguments.js": "FAIL", + "language/eval-code/direct/func-decl-fn-body-cntns-arguments-lex-bind-declare-arguments-and-assign.js": "FAIL", + "language/eval-code/direct/func-decl-fn-body-cntns-arguments-lex-bind-declare-arguments.js": "FAIL", + "language/eval-code/direct/func-decl-fn-body-cntns-arguments-var-bind-declare-arguments-and-assign.js": "FAIL", + "language/eval-code/direct/func-decl-fn-body-cntns-arguments-var-bind-declare-arguments.js": "FAIL", + "language/eval-code/direct/func-decl-no-pre-existing-arguments-bindings-are-present-declare-arguments-and-assign.js": "FAIL", + "language/eval-code/direct/func-decl-no-pre-existing-arguments-bindings-are-present-declare-arguments.js": "FAIL", + "language/eval-code/direct/func-expr-fn-body-cntns-arguments-func-decl-declare-arguments-and-assign.js": "FAIL", + "language/eval-code/direct/func-expr-fn-body-cntns-arguments-func-decl-declare-arguments.js": "FAIL", + "language/eval-code/direct/func-expr-fn-body-cntns-arguments-lex-bind-declare-arguments-and-assign.js": "FAIL", + "language/eval-code/direct/func-expr-fn-body-cntns-arguments-lex-bind-declare-arguments.js": "FAIL", + "language/eval-code/direct/func-expr-fn-body-cntns-arguments-var-bind-declare-arguments-and-assign.js": "FAIL", + "language/eval-code/direct/func-expr-fn-body-cntns-arguments-var-bind-declare-arguments.js": "FAIL", + "language/eval-code/direct/func-expr-no-pre-existing-arguments-bindings-are-present-declare-arguments-and-assign.js": "FAIL", + "language/eval-code/direct/func-expr-no-pre-existing-arguments-bindings-are-present-declare-arguments.js": "FAIL", "language/eval-code/direct/gen-func-decl-a-following-parameter-is-named-arguments-declare-arguments-and-assign.js": "FAIL", "language/eval-code/direct/gen-func-decl-a-following-parameter-is-named-arguments-declare-arguments.js": "FAIL", "language/eval-code/direct/gen-func-decl-a-preceding-parameter-is-named-arguments-declare-arguments-and-assign.js": "FAIL", @@ -9536,6 +9554,14 @@ "language/eval-code/direct/gen-meth-no-pre-existing-arguments-bindings-are-present-declare-arguments-and-assign.js": "FAIL", "language/eval-code/direct/gen-meth-no-pre-existing-arguments-bindings-are-present-declare-arguments.js": "FAIL", "language/eval-code/direct/global-env-rec-with.js": "CRASH", + "language/eval-code/direct/meth-fn-body-cntns-arguments-func-decl-declare-arguments-and-assign.js": "FAIL", + "language/eval-code/direct/meth-fn-body-cntns-arguments-func-decl-declare-arguments.js": "FAIL", + "language/eval-code/direct/meth-fn-body-cntns-arguments-lex-bind-declare-arguments-and-assign.js": "FAIL", + "language/eval-code/direct/meth-fn-body-cntns-arguments-lex-bind-declare-arguments.js": "FAIL", + "language/eval-code/direct/meth-fn-body-cntns-arguments-var-bind-declare-arguments-and-assign.js": "FAIL", + "language/eval-code/direct/meth-fn-body-cntns-arguments-var-bind-declare-arguments.js": "FAIL", + "language/eval-code/direct/meth-no-pre-existing-arguments-bindings-are-present-declare-arguments-and-assign.js": "FAIL", + "language/eval-code/direct/meth-no-pre-existing-arguments-bindings-are-present-declare-arguments.js": "FAIL", "language/eval-code/direct/new.target-fn.js": "FAIL", "language/eval-code/direct/super-prop-method.js": "FAIL", "language/eval-code/indirect/always-non-strict.js": "FAIL", @@ -16956,6 +16982,8 @@ "language/statements/function/S13.2.2_A19_T6.js": "CRASH", "language/statements/function/S13.2.2_A19_T7.js": "CRASH", "language/statements/function/S13.2.2_A19_T8.js": "CRASH", + "language/statements/function/S13_A15_T4.js": "FAIL", + "language/statements/function/S13_A15_T5.js": "FAIL", "language/statements/function/S13_A19_T1.js": "FAIL", "language/statements/function/S13_A6_T1.js": "FAIL", "language/statements/function/S13_A6_T2.js": "FAIL", @@ -17903,6 +17931,7 @@ "staging/sm/destructuring/iterator-primitive.js": "FAIL", "staging/sm/destructuring/order-super.js": "CRASH", "staging/sm/destructuring/order.js": "FAIL", + "staging/sm/eval/redeclared-arguments-in-param-expression-eval.js": "FAIL", "staging/sm/expressions/ToPropertyKey-symbols.js": "CRASH", "staging/sm/expressions/delete-name-parenthesized-early-error-strict-mode.js": "FAIL", "staging/sm/expressions/destructuring-array-default-class.js": "CRASH", @@ -18027,6 +18056,7 @@ "staging/sm/object/values.js": "FAIL", "staging/sm/regress/regress-1383630.js": "FAIL", "staging/sm/regress/regress-1507322-deep-weakmap.js": "FAIL", + "staging/sm/regress/regress-162392.js": "FAIL", "staging/sm/regress/regress-325925.js": "FAIL", "staging/sm/regress/regress-428366.js": "FAIL", "staging/sm/regress/regress-449666.js": "FAIL", diff --git a/tests/metrics.json b/tests/metrics.json index d0b07d469..763d606ee 100644 --- a/tests/metrics.json +++ b/tests/metrics.json @@ -1,8 +1,8 @@ { "results": { "crash": 6728, - "fail": 11374, - "pass": 28996, + "fail": 11404, + "pass": 28966, "skip": 30, "timeout": 4, "unresolved": 0