From c94e10ddd67068a35eb041a2ee287dd832a5969f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Juli=C3=A1n=20Espina?= Date: Thu, 22 Feb 2024 15:19:28 -0600 Subject: [PATCH] Change `HostEnsureCanCompileStrings` to the new spec (#3690) * Change `HostEnsureCanCompileStrings` to the new spec * Fix test --- core/engine/src/builtins/eval/mod.rs | 6 +- core/engine/src/builtins/function/mod.rs | 188 ++++++++++-------- core/engine/src/context/hooks.rs | 21 +- .../src/object/builtins/jsarraybuffer.rs | 5 +- core/engine/src/vm/call_frame/mod.rs | 3 - 5 files changed, 129 insertions(+), 94 deletions(-) diff --git a/core/engine/src/builtins/eval/mod.rs b/core/engine/src/builtins/eval/mod.rs index 995772bc3f..3007340c23 100644 --- a/core/engine/src/builtins/eval/mod.rs +++ b/core/engine/src/builtins/eval/mod.rs @@ -108,10 +108,12 @@ impl Eval { // 3. Let evalRealm be the current Realm Record. // 4. NOTE: In the case of a direct eval, evalRealm is the realm of both the caller of eval // and of the eval function itself. - // 5. Perform ? HostEnsureCanCompileStrings(evalRealm). + let eval_realm = context.realm().clone(); + + // 5. Perform ? HostEnsureCanCompileStrings(evalRealm, « », x, direct). context .host_hooks() - .ensure_can_compile_strings(context.realm().clone(), context)?; + .ensure_can_compile_strings(eval_realm, &[], x, direct, context)?; // 11. Perform the following substeps in an implementation-defined order, possibly interleaving parsing and error detection: // a. Let script be ParseText(StringToCodePoints(x), Script). diff --git a/core/engine/src/builtins/function/mod.rs b/core/engine/src/builtins/function/mod.rs index 771ff4f09a..20a05cd71f 100644 --- a/core/engine/src/builtins/function/mod.rs +++ b/core/engine/src/builtins/function/mod.rs @@ -45,7 +45,6 @@ use boa_gc::{self, custom_trace, Finalize, Gc, Trace}; use boa_interner::Sym; use boa_parser::{Parser, Source}; use boa_profiler::Profiler; -use std::io::Read; use thin_vec::ThinVec; use super::Proxy; @@ -390,13 +389,7 @@ impl BuiltInFunctionObject { generator: bool, context: &mut Context, ) -> JsResult { - // 1. Let currentRealm be the current Realm Record. - // 2. Perform ? HostEnsureCanCompileStrings(currentRealm). - context - .host_hooks() - .ensure_can_compile_strings(context.realm().clone(), context)?; - - // 3. If newTarget is undefined, set newTarget to constructor. + // 1. If newTarget is undefined, set newTarget to constructor. let new_target = if new_target.is_undefined() { constructor.into() } else { @@ -404,8 +397,8 @@ impl BuiltInFunctionObject { }; let default = if r#async && generator { - // 7. Else, - // a. Assert: kind is asyncGenerator. + // 5. Else, + // a. Assert: kind is async-generator. // b. Let prefix be "async function*". // c. Let exprSym be the grammar symbol AsyncGeneratorExpression. // d. Let bodySym be the grammar symbol AsyncGeneratorBody. @@ -413,7 +406,7 @@ impl BuiltInFunctionObject { // f. Let fallbackProto be "%AsyncGeneratorFunction.prototype%". StandardConstructors::async_generator_function } else if r#async { - // 6. Else if kind is async, then + // 4. Else if kind is async, then // a. Let prefix be "async function". // b. Let exprSym be the grammar symbol AsyncFunctionExpression. // c. Let bodySym be the grammar symbol AsyncFunctionBody. @@ -421,7 +414,7 @@ impl BuiltInFunctionObject { // e. Let fallbackProto be "%AsyncFunction.prototype%". StandardConstructors::async_function } else if generator { - // 5. Else if kind is generator, then + // 3. Else if kind is generator, then // a. Let prefix be "function*". // b. Let exprSym be the grammar symbol GeneratorExpression. // c. Let bodySym be the grammar symbol GeneratorBody. @@ -429,7 +422,7 @@ impl BuiltInFunctionObject { // e. Let fallbackProto be "%GeneratorFunction.prototype%". StandardConstructors::generator_function } else { - // 4. If kind is normal, then + // 2. If kind is normal, then // a. Let prefix be "function". // b. Let exprSym be the grammar symbol FunctionExpression. // c. Let bodySym be the grammar symbol FunctionBody[~Yield, ~Await]. @@ -441,74 +434,119 @@ impl BuiltInFunctionObject { // 22. Let proto be ? GetPrototypeFromConstructor(newTarget, fallbackProto). let prototype = get_prototype_from_constructor(&new_target, default, context)?; - let (parameters, body) = if let Some((body_arg, args)) = args.split_last() { - let parameters = if args.is_empty() { - FormalParameterList::default() - } else { - let mut parameters = Vec::with_capacity(args.len()); - for arg in args { - parameters.push(arg.to_string(context)?); - } - let parameters = parameters.join(utf16!(",")); - - // TODO: make parser generic to u32 iterators - let parameters = String::from_utf16_lossy(¶meters); - let mut parser = Parser::new(Source::from_bytes(¶meters)); - parser.set_identifier(context.next_parser_identifier()); - - let parameters = match parser.parse_formal_parameters( - context.interner_mut(), - generator, - r#async, - ) { - Ok(parameters) => parameters, - Err(e) => { - return Err(JsNativeError::syntax() - .with_message(format!("failed to parse function parameters: {e}")) - .into()) - } - }; + // 6. Let argCount be the number of elements in parameterArgs. + let (body, param_list) = if let Some((body, params)) = args.split_last() { + // 7. Let bodyString be ? ToString(bodyArg). + let body = body.to_string(context)?; + // 8. Let parameterStrings be a new empty List. + let mut parameters = Vec::with_capacity(args.len()); + // 9. For each element arg of parameterArgs, do + for param in params { + // a. Append ? ToString(arg) to parameterStrings. + parameters.push(param.to_string(context)?); + } + (body, parameters) + } else { + (js_string!(), Vec::new()) + }; + let current_realm = context.realm().clone(); - if generator && contains(¶meters, ContainsSymbol::YieldExpression) { - return Err(JsNativeError::syntax().with_message( - "yield expression is not allowed in formal parameter list of generator function", - ).into()); - } + context.host_hooks().ensure_can_compile_strings( + current_realm, + ¶m_list, + &body, + false, + context, + )?; - parameters - }; + let parameters = if param_list.is_empty() { + FormalParameterList::default() + } else { + // 12. Let P be the empty String. + // 13. If argCount > 0, then + // a. Set P to parameterStrings[0]. + // b. Let k be 1. + // c. Repeat, while k < argCount, + // i. Let nextArgString be parameterStrings[k]. + // ii. Set P to the string-concatenation of P, "," (a comma), and nextArgString. + // iii. Set k to k + 1. + let parameters = param_list.join(utf16!(",")); + let mut parser = Parser::new(Source::from_utf16(¶meters)); + parser.set_identifier(context.next_parser_identifier()); + + // 17. Let parameters be ParseText(StringToCodePoints(P), parameterSym). + // 18. If parameters is a List of errors, throw a SyntaxError exception. + let parameters = parser + .parse_formal_parameters(context.interner_mut(), generator, r#async) + .map_err(|e| { + JsNativeError::syntax() + .with_message(format!("failed to parse function parameters: {e}")) + })?; // It is a Syntax Error if FormalParameters Contains YieldExpression is true. - if generator && r#async && contains(¶meters, ContainsSymbol::YieldExpression) { - return Err(JsNativeError::syntax() - .with_message("yield expression not allowed in async generator parameters") - .into()); + if generator && contains(¶meters, ContainsSymbol::YieldExpression) { + return Err(JsNativeError::syntax().with_message( + if r#async { + "yield expression is not allowed in formal parameter list of async generator" + } else { + "yield expression is not allowed in formal parameter list of generator" + } + ).into()); } // It is a Syntax Error if FormalParameters Contains AwaitExpression is true. - if generator && r#async && contains(¶meters, ContainsSymbol::AwaitExpression) { + if r#async && contains(¶meters, ContainsSymbol::AwaitExpression) { return Err(JsNativeError::syntax() - .with_message("await expression not allowed in async generator parameters") + .with_message( + if generator { + "await expression is not allowed in formal parameter list of async function" + } else { + "await expression is not allowed in formal parameter list of async generator" + }) .into()); } - // 11. Let bodyString be the string-concatenation of 0x000A (LINE FEED), ? ToString(bodyArg), and 0x000A (LINE FEED). - let body_arg = body_arg.to_string(context)?.to_std_string_escaped(); - let body = b"\n".chain(body_arg.as_bytes()).chain(b"\n".as_slice()); + parameters + }; - // TODO: make parser generic to u32 iterators - let mut parser = Parser::new(Source::from_reader(body, None)); + let body = if body.is_empty() { + FunctionBody::default() + } else { + // 14. Let bodyParseString be the string-concatenation of 0x000A (LINE FEED), bodyString, and 0x000A (LINE FEED). + let mut body_parse = Vec::with_capacity(body.len()); + body_parse.push(u16::from(b'\n')); + body_parse.extend_from_slice(&body); + body_parse.push(u16::from(b'\n')); + + // 19. Let body be ParseText(StringToCodePoints(bodyParseString), bodySym). + // 20. If body is a List of errors, throw a SyntaxError exception. + let mut parser = Parser::new(Source::from_utf16(&body_parse)); parser.set_identifier(context.next_parser_identifier()); - let body = match parser.parse_function_body(context.interner_mut(), generator, r#async) - { - Ok(statement_list) => statement_list, - Err(e) => { - return Err(JsNativeError::syntax() + // 19. Let body be ParseText(StringToCodePoints(bodyParseString), bodySym). + // 20. If body is a List of errors, throw a SyntaxError exception. + let body = parser + .parse_function_body(context.interner_mut(), generator, r#async) + .map_err(|e| { + JsNativeError::syntax() .with_message(format!("failed to parse function body: {e}")) - .into()) - } - }; + })?; + + // It is a Syntax Error if AllPrivateIdentifiersValid of StatementList with argument « » + // is false unless the source text containing ScriptBody is eval code that is being + // processed by a direct eval. + // https://tc39.es/ecma262/#sec-scripts-static-semantics-early-errors + if !all_private_identifiers_valid(&body, Vec::new()) { + return Err(JsNativeError::syntax() + .with_message("invalid private identifier usage") + .into()); + } + + // 21. NOTE: The parameters and body are parsed separately to ensure that each is valid alone. For example, new Function("/*", "*/ ) {") does not evaluate to a function. + // 22. NOTE: If this step is reached, sourceText must have the syntax of exprSym (although the reverse implication does not hold). The purpose of the next two steps is to enforce any Early Error rules which apply to exprSym directly. + // 23. Let expr be ParseText(sourceText, exprSym). + // 24. If expr is a List of errors, throw a SyntaxError exception. + // Check for errors that apply for the combination of body and parameters. // Early Error: If BindingIdentifier is present and the source text matched by BindingIdentifier is strict mode code, // it is a Syntax Error if the StringValue of BindingIdentifier is "eval" or "arguments". @@ -516,7 +554,7 @@ impl BuiltInFunctionObject { for name in bound_names(¶meters) { if name == Sym::ARGUMENTS || name == Sym::EVAL { return Err(JsNativeError::syntax() - .with_message(" Unexpected 'eval' or 'arguments' in strict mode") + .with_message("Unexpected 'eval' or 'arguments' in strict mode") .into()); } } @@ -571,21 +609,7 @@ impl BuiltInFunctionObject { } } - if !all_private_identifiers_valid(¶meters, Vec::new()) { - return Err(JsNativeError::syntax() - .with_message("invalid private identifier usage") - .into()); - } - - if !all_private_identifiers_valid(&body, Vec::new()) { - return Err(JsNativeError::syntax() - .with_message("invalid private identifier usage") - .into()); - } - - (parameters, body) - } else { - (FormalParameterList::default(), FunctionBody::default()) + body }; let code = FunctionCompiler::new() diff --git a/core/engine/src/context/hooks.rs b/core/engine/src/context/hooks.rs index 31f66eb35b..3ddc0bc2a3 100644 --- a/core/engine/src/context/hooks.rs +++ b/core/engine/src/context/hooks.rs @@ -4,7 +4,7 @@ use crate::{ job::JobCallback, object::{JsFunction, JsObject}, realm::Realm, - Context, JsResult, JsValue, + Context, JsResult, JsString, JsValue, }; use time::{OffsetDateTime, UtcOffset}; @@ -25,7 +25,7 @@ use time::util::local_offset; /// use boa_engine::{ /// context::{Context, ContextBuilder, HostHooks}, /// realm::Realm, -/// JsNativeError, JsResult, Source, +/// JsNativeError, JsResult, JsString, Source, /// }; /// /// struct Hooks; @@ -34,7 +34,10 @@ use time::util::local_offset; /// fn ensure_can_compile_strings( /// &self, /// _realm: Realm, -/// context: &mut Context, +/// _parameters: &[JsString], +/// _body: &JsString, +/// _direct: bool, +/// _context: &mut Context, /// ) -> JsResult<()> { /// Err(JsNativeError::typ() /// .with_message("eval calls not available") @@ -106,7 +109,7 @@ pub trait HostHooks { // The default implementation of HostPromiseRejectionTracker is to return unused. } - /// [`HostEnsureCanCompileStrings ( calleeRealm )`][spec] + /// [`HostEnsureCanCompileStrings ( calleeRealm, parameterStrings, bodyString, direct )`][spec] /// /// # Requirements /// @@ -114,8 +117,14 @@ pub trait HostHooks { /// containing unused. This is already ensured by the return type. /// /// [spec]: https://tc39.es/ecma262/#sec-hostensurecancompilestrings - // TODO: Track https://github.com/tc39/ecma262/issues/938 - fn ensure_can_compile_strings(&self, _realm: Realm, _context: &mut Context) -> JsResult<()> { + fn ensure_can_compile_strings( + &self, + _realm: Realm, + _parameters: &[JsString], + _body: &JsString, + _direct: bool, + _context: &mut Context, + ) -> JsResult<()> { // The default implementation of HostEnsureCanCompileStrings is to return NormalCompletion(unused). Ok(()) } diff --git a/core/engine/src/object/builtins/jsarraybuffer.rs b/core/engine/src/object/builtins/jsarraybuffer.rs index 339be87925..259a4c93e6 100644 --- a/core/engine/src/object/builtins/jsarraybuffer.rs +++ b/core/engine/src/object/builtins/jsarraybuffer.rs @@ -230,7 +230,10 @@ impl JsArrayBuffer { /// // Get a reference to the data. /// let internal_buffer = array_buffer.data(); /// - /// assert_eq!(internal_buffer.as_deref(), Some((0..5).collect::>().as_slice())); + /// assert_eq!( + /// internal_buffer.as_deref(), + /// Some((0..5).collect::>().as_slice()) + /// ); /// # Ok(()) /// # } /// ``` diff --git a/core/engine/src/vm/call_frame/mod.rs b/core/engine/src/vm/call_frame/mod.rs index c79ba394e0..e994f4dd00 100644 --- a/core/engine/src/vm/call_frame/mod.rs +++ b/core/engine/src/vm/call_frame/mod.rs @@ -40,7 +40,6 @@ pub struct CallFrame { pub(crate) code_block: Gc, pub(crate) pc: u32, /// The register pointer, points to the first register in the stack. - /// // TODO: Check if storing the frame pointer instead of argument count and computing the // argument count based on the pointers would be better for accessing the arguments // and the elements before the register pointer. @@ -124,13 +123,11 @@ impl CallFrame { /// │ callee frame pointer /// │ /// └───── caller frame pointer - /// /// ``` /// /// Some questions: /// /// - Who is responsible for cleaning up the stack after a call? The rust caller. - /// pub(crate) const FUNCTION_PROLOGUE: u32 = 2; pub(crate) const THIS_POSITION: u32 = 2; pub(crate) const FUNCTION_POSITION: u32 = 1;