From 7941e726c58242b60199b23023557d7ac8c8879c Mon Sep 17 00:00:00 2001 From: Taylor Woll Date: Wed, 1 Nov 2017 17:30:54 -0700 Subject: [PATCH] Support for defer parse of object literal methods This is mostly a straightforward modification to the defer-parse methods in the parser to enable them to know how to handle the object literal method grammar. Communicate the fact that the deferred function is a method via a parser flag - `fscrDeferredFncIsMethod`. --- lib/Parser/Parse.cpp | 131 +++++++++++++---- lib/Parser/ParseFlags.h | 5 +- lib/Runtime/Base/FunctionBody.cpp | 14 +- lib/Runtime/Base/FunctionBody.h | 9 +- lib/Runtime/ByteCode/ByteCodeGenerator.cpp | 1 + test/es6/DeferParseMethods.js | 161 +++++++++++++++++++++ test/es6/rlexe.xml | 36 +++++ 7 files changed, 321 insertions(+), 36 deletions(-) create mode 100644 test/es6/DeferParseMethods.js diff --git a/lib/Parser/Parse.cpp b/lib/Parser/Parse.cpp index 702937cf086..96bc5d2db08 100644 --- a/lib/Parser/Parse.cpp +++ b/lib/Parser/Parse.cpp @@ -3215,6 +3215,14 @@ ParseNodePtr Parser::ParseTerm(BOOL fAllowCall, pid = ParseSuper(!!fAllowCall); isSpecialName = true; + // Super reference and super call need to push a pid ref to 'this' even when not building an AST + ReferenceSpecialName(wellKnownPropertyPids._this, ichMin, ichLim); + // Super call needs to reference 'new.target' + if (pid == wellKnownPropertyPids._superConstructor) + { + ReferenceSpecialName(wellKnownPropertyPids._newTarget, ichMin, ichLim); + } + goto LIdentifier; case tkTHIS: @@ -4529,6 +4537,11 @@ ParseNodePtr Parser::ParseMemberList(LPCOLESTR pNameHint, uint32* pNameHintLengt { Error(ERRsyntax); } + + // Include star character in the function extents + ichMin = m_pscan->IchMinTok(); + iecpMin = m_pscan->IecpMinTok(); + m_pscan->ScanForcingPid(); fncDeclFlags |= fFncGenerator; } @@ -4702,7 +4715,7 @@ ParseNodePtr Parser::ParseMemberList(LPCOLESTR pNameHint, uint32* pNameHintLengt ParseNodePtr pnodeFunc = ParseFncDecl(fncDeclFlags | (isAsyncMethod ? fFncAsync : fFncNoFlgs), pFullNameHint, /*needsPIDOnRCurlyScan*/ false, /*resetParsingSuperRestrictionState*/ false); - if (isAsyncMethod) + if (isAsyncMethod || isGenerator) { pnodeFunc->sxFnc.cbMin = iecpMin; pnodeFunc->ichMin = ichMin; @@ -5277,7 +5290,6 @@ bool Parser::ParseFncDeclHelper(ParseNodePtr pnodeFnc, LPCOLESTR pNameHint, usho // switch scanner to treat 'yield' as keyword in generator functions // or as an identifier in non-generator functions bool fPreviousYieldIsKeyword = m_pscan->SetYieldIsKeywordRegion(pnodeFnc && pnodeFnc->sxFnc.IsGenerator()); - bool fPreviousAwaitIsKeyword = m_pscan->SetAwaitIsKeywordRegion(fAsync); if (pnodeFnc && pnodeFnc->sxFnc.IsGenerator()) @@ -5318,10 +5330,10 @@ bool Parser::ParseFncDeclHelper(ParseNodePtr pnodeFnc, LPCOLESTR pNameHint, usho } uint uDeferSave = m_grfscr & fscrDeferFncParse; - if (flags & fFncNoName) + if (flags & fFncClassMember) { - // Disable deferral on getter/setter or other construct with unusual text bounds - // (fFncNoName) as these are usually trivial, and re-parsing is problematic. + // Disable deferral on class members or other construct with unusual text bounds + // as these are usually trivial, and re-parsing is problematic. // NOTE: It is probably worth supporting these cases for memory and load-time purposes, // especially as they become more and more common. m_grfscr &= ~fscrDeferFncParse; @@ -7129,6 +7141,7 @@ void Parser::FinishFncNode(ParseNodePtr pnodeFnc) m_pnestedCount = &pnodeFnc->sxFnc.nestedCount; bool fLambda = pnodeFnc->sxFnc.IsLambda(); + bool fMethod = pnodeFnc->sxFnc.IsMethod(); // Cue up the parser to the start of the function body. if (pnodeFnc->sxFnc.pnodeName) @@ -7139,7 +7152,30 @@ void Parser::FinishFncNode(ParseNodePtr pnodeFnc) else { m_pscan->SetCurrentCharacter(pnodeFnc->ichMin, pnodeFnc->sxFnc.lineNumber); - if (pnodeFnc->sxFnc.IsAccessor()) + + if (fMethod) + { + // Method. Skip identifier name, computed property name, "async", "get", "set", and '*' or '(' characters. + for (;;) + { + m_pscan->Scan(); + // '[' character indicates a computed property name for this method. We should consume it. + if (m_token.tk == tkLBrack) + { + // We don't care what the name expr is. + m_pscan->Scan(); + ParseExpr(); + Assert(m_token.tk == tkRBrack); + continue; + } + // Quit scanning ahead when we reach a '(' character which opens the arg list. + if (m_token.tk == tkLParen) + { + break; + } + } + } + else if (pnodeFnc->sxFnc.IsAccessor()) { // Getter/setter. The node text starts with the name, so eat that. m_pscan->ScanNoKeywords(); @@ -7171,7 +7207,11 @@ void Parser::FinishFncNode(ParseNodePtr pnodeFnc) bool fPreviousAwaitIsKeyword = m_pscan->SetAwaitIsKeywordRegion(pnodeFnc && pnodeFnc->sxFnc.IsAsync()); // Skip the arg list. - m_pscan->Scan(); + if (!fMethod) + { + // If this is a method, we've already advanced to the '(' token. + m_pscan->Scan(); + } if (m_token.tk == tkStar) { Assert(pnodeFnc->sxFnc.IsGenerator()); @@ -11517,7 +11557,9 @@ ParseNodePtr Parser::Parse(LPCUTF8 pszSrc, size_t offset, size_t length, charcou ushort flags = fFncNoFlgs; size_t iecpMin = 0; charcount_t ichMin = 0; - bool isAsyncMethod = false; + bool isAsync = false; + bool isGenerator = false; + bool isMethod = false; // The top-level deferred function body was defined by a function expression whose parsing was deferred. We are now // parsing it, so unset the flag so that any nested functions are parsed normally. This flag is only applicable the @@ -11536,49 +11578,73 @@ ParseNodePtr Parser::Parse(LPCUTF8 pszSrc, size_t offset, size_t length, charcou flags |= fFncDeclaration; } - // There are three cases which can confirm async function: - // async function... -> async function - // async (... -> async lambda with parens around the formal parameter - // async identifier... -> async lambda with single identifier parameter + if (m_grfscr & fscrDeferredFncIsMethod) + { + m_grfscr &= ~fscrDeferredFncIsMethod; + isMethod = true; + flags |= fFncNoName | fFncMethod; + } + + // These are the cases which can confirm async function: + // async function() {} -> async function + // async () => {} -> async lambda with parens around the formal parameter + // async arg => {} -> async lambda with single identifier parameter + // async name() {} -> async method + // async [computed_name]() {} -> async method with a computed name if (m_token.tk == tkID && m_token.GetIdentifier(m_phtbl) == wellKnownPropertyPids.async && m_scriptContext->GetConfig()->IsES7AsyncAndAwaitEnabled()) { ichMin = m_pscan->IchMinTok(); iecpMin = m_pscan->IecpMinTok(); - // Keep state so we can rewind if it turns out that this isn't an async function. - // The only way this can happen is if we have a lambda with a single formal parameter named 'async' not enclosed by parens. + // Keep state so we can rewind if it turns out that this isn't an async function: + // async() {} -> method named async + // async => {} -> lambda with single parameter named async RestorePoint termStart; m_pscan->Capture(&termStart); m_pscan->Scan(); - if ((m_token.tk == tkID || m_token.tk == tkLParen || m_token.tk == tkFUNCTION) && !m_pscan->FHadNewLine()) + + if (m_token.tk == tkDArrow || (m_token.tk == tkLParen && isMethod) || m_pscan->FHadNewLine()) { - flags |= fFncAsync; - isAsyncMethod = true; + m_pscan->SeekTo(termStart); } else { - m_pscan->SeekTo(termStart); + flags |= fFncAsync; + isAsync = true; } } - // If first token of the function is tkID or tkLParen, this is a lambda. - if (m_token.tk == tkID || m_token.tk == tkLParen) + if (m_token.tk == tkStar && m_scriptContext->GetConfig()->IsES6GeneratorsEnabled()) { - flags |= fFncLambda; + ichMin = m_pscan->IchMinTok(); + iecpMin = m_pscan->IecpMinTok(); + + flags |= fFncGenerator; + isGenerator = true; + + m_pscan->Scan(); } - else + + // Eat the computed name expression + if (m_token.tk == tkLBrack && isMethod) { - // Must be ordinary function keyword - do not eat the token - ChkCurTokNoScan(tkFUNCTION, ERRsyntax); + m_pscan->Scan(); + ParseExpr(); + } + + if (!isMethod && (m_token.tk == tkID || m_token.tk == tkLParen)) + { + // If first token of the function is tkID or tkLParen, this is a lambda. + flags |= fFncLambda; } ParseNodePtr pnodeFnc = ParseFncDecl(flags, nullptr, false, false); pnodeProg->sxFnc.pnodeBody = nullptr; AddToNodeList(&pnodeProg->sxFnc.pnodeBody, &lastNodeRef, pnodeFnc); - // Include the async keyword in the function extents - if (isAsyncMethod) + // Include the async keyword or star character in the function extents + if (isAsync || isGenerator) { pnodeFnc->sxFnc.cbMin = iecpMin; pnodeFnc->ichMin = ichMin; @@ -12519,6 +12585,16 @@ IdentPtr Parser::ParseSuper(bool fAllowCall) break; } + currentNodeFunc->sxFnc.SetHasSuperReference(TRUE); + CHAKRATEL_LANGSTATS_INC_LANGFEATURECOUNT(Super, m_scriptContext); + + // If we are defer parsing, we can skip verifying that the super reference is valid. + // If it wasn't the parser would have thrown during upfront parsing and we wouldn't be defer parsing the function. + if (m_parseType == ParseType_Deferred) + { + return superPid; + } + if (!fAllowCall && (m_token.tk == tkLParen)) { Error(ERRInvalidSuper); // new super() is not allowed @@ -12556,9 +12632,6 @@ IdentPtr Parser::ParseSuper(bool fAllowCall) // Anything else is an error Error(ERRInvalidSuper); } - - currentNodeFunc->sxFnc.SetHasSuperReference(TRUE); - CHAKRATEL_LANGSTATS_INC_LANGFEATURECOUNT(Super, m_scriptContext); return superPid; } diff --git a/lib/Parser/ParseFlags.h b/lib/Parser/ParseFlags.h index 0e6c75bb541..9a1d31d2a8f 100644 --- a/lib/Parser/ParseFlags.h +++ b/lib/Parser/ParseFlags.h @@ -51,6 +51,7 @@ enum // let/const in global scope instead of eval scope so that they can be preserved across console inputs fscrNoAsmJs = 1 << 25, // Disable generation of asm.js code fscrIsModuleCode = 1 << 26, // Current code should be parsed as a module body - fscrAll = (1 << 27) - 1 -}; + fscrDeferredFncIsMethod = 1 << 27, + fscrAll = (1 << 28) - 1 +}; diff --git a/lib/Runtime/Base/FunctionBody.cpp b/lib/Runtime/Base/FunctionBody.cpp index 6e1b2224760..58d8e99f593 100644 --- a/lib/Runtime/Base/FunctionBody.cpp +++ b/lib/Runtime/Base/FunctionBody.cpp @@ -1605,7 +1605,8 @@ namespace Js paramScopeSlotArraySize(0), m_reparsed(false), m_isAsmJsFunction(false), - m_tag21(true) + m_tag21(true), + m_isMethod(false) #if DBG ,m_wasEverAsmjsMode(false) ,scopeObjectSize(0) @@ -1655,6 +1656,7 @@ namespace Js m_isStaticNameFunction(proxy->GetIsStaticNameFunction()), m_reportedInParamCount(proxy->GetReportedInParamsCount()), m_reparsed(proxy->IsReparsed()), + m_isMethod(proxy->IsMethod()), m_tag21(true) #if DBG ,m_wasEverAsmjsMode(proxy->m_wasEverAsmjsMode) @@ -2336,6 +2338,16 @@ namespace Js // (not a function declaration statement). grfscr |= fscrDeferredFncExpression; } + + if (funcBody->IsMethod()) + { + grfscr |= fscrDeferredFncIsMethod; + } + else + { + grfscr &= ~fscrDeferredFncIsMethod; + } + if (!CONFIG_FLAG(DeferNested) || isDebugOrAsmJsReparse) { grfscr &= ~fscrDeferFncParse; // Disable deferred parsing if not DeferNested, or doing a debug/asm.js re-parse diff --git a/lib/Runtime/Base/FunctionBody.h b/lib/Runtime/Base/FunctionBody.h index e8f0987f65b..4dae69c3c4b 100644 --- a/lib/Runtime/Base/FunctionBody.h +++ b/lib/Runtime/Base/FunctionBody.h @@ -2025,6 +2025,8 @@ namespace Js LPCUTF8 GetStartOfDocument(const char16* reason = nullptr) const; bool IsReparsed() const { return m_reparsed; } void SetReparsed(bool set) { m_reparsed = set; } + bool IsMethod() const { return m_isMethod; } + void SetIsMethod(bool set) { m_isMethod = set; } bool GetExternalDisplaySourceName(BSTR* sourceName); void CleanupToReparse(); @@ -2159,10 +2161,9 @@ namespace Js FieldWithBarrier(bool) m_isEval : 1; // Source code is in 'eval' FieldWithBarrier(bool) m_isDynamicFunction : 1; // Source code is in 'Function' FieldWithBarrier(bool) m_hasImplicitArgIns : 1; - FieldWithBarrier(bool) m_dontInline : 1; // Used by the JIT's inliner - - // Indicates if the function has been reparsed for debug attach/detach scenario. - FieldWithBarrier(bool) m_reparsed : 1; + FieldWithBarrier(bool) m_dontInline : 1; // Used by the JIT's inliner + FieldWithBarrier(bool) m_reparsed : 1; // Indicates if the function has been reparsed for debug attach/detach scenario. + FieldWithBarrier(bool) m_isMethod : 1; // Function is an object literal method // This field is not required for deferred parsing but because our thunks can't handle offsets > 128 bytes // yet, leaving this here for now. We can look at optimizing the function info and function proxy structures some diff --git a/lib/Runtime/ByteCode/ByteCodeGenerator.cpp b/lib/Runtime/ByteCode/ByteCodeGenerator.cpp index 810da82a3e3..b63a195d767 100644 --- a/lib/Runtime/ByteCode/ByteCodeGenerator.cpp +++ b/lib/Runtime/ByteCode/ByteCodeGenerator.cpp @@ -1403,6 +1403,7 @@ FuncInfo * ByteCodeGenerator::StartBindFunction(const char16 *name, uint nameLen parseableFunctionInfo->deferredParseNextFunctionId = pnode->sxFnc.deferredParseNextFunctionId; #endif parseableFunctionInfo->SetIsDeclaration(pnode->sxFnc.IsDeclaration() != 0); + parseableFunctionInfo->SetIsMethod(pnode->sxFnc.IsMethod() != 0); parseableFunctionInfo->SetIsAccessor(pnode->sxFnc.IsAccessor() != 0); if (pnode->sxFnc.IsAccessor()) { diff --git a/test/es6/DeferParseMethods.js b/test/es6/DeferParseMethods.js new file mode 100644 index 00000000000..0981b0fccc6 --- /dev/null +++ b/test/es6/DeferParseMethods.js @@ -0,0 +1,161 @@ +//------------------------------------------------------------------------------------------------------- +// Copyright (C) Microsoft. All rights reserved. +// Licensed under the MIT license. See LICENSE.txt file in the project root for full license information. +//------------------------------------------------------------------------------------------------------- + +WScript.LoadScriptFile("..\\UnitTestFramework\\UnitTestFramework.js"); + +var tests = [ + { + name: "Various object literal method deferral", + body: function () { + var sym = Symbol(); + let out = 'nothing'; + var obj = { + get a() { return 'get a'; }, + set a(v) { out = 'set a'; }, + b() { return 'b'; }, + ['c']() { return 'c'; }, + [sym]() { return 'sym'; }, + async d() { return 'd'; }, + *e() { yield 'e'; }, + get ['f']() { return 'get f'; }, + set ['f'](v) { out = 'set f'; }, + async ['g']() { return 'g'; }, + *['h']() { yield 'h'; }, + async async() { return 'async async'; }, + } + var obj2 = { + async() { return 'async'; } + } + var obj3 = { + get async() { return 'get async'; }, + set async(v) { out = 'set async'; } + } + var obj4 = { + *async() { yield 'generator async'; } + } + + assert.areEqual('get a', obj.a, "Simple named getter"); + obj.a = 123; + assert.areEqual('set a', out, "Simple named setter"); + assert.areEqual('b', obj.b(), "Simple method"); + + assert.areEqual('c', obj.c(), "Method with computed property name"); + assert.areEqual('sym', obj[sym](), "Method with computed property name (key is not string)"); + + assert.isTrue(obj.d() instanceof Promise, "Async method"); + + assert.areEqual('e', obj.e().next().value, "Generator method"); + + assert.areEqual('get f', obj.f, "Getter method with computed name"); + obj.f = 123; + assert.areEqual('set f', out, "Setter method with computed name"); + + assert.isTrue(obj.g() instanceof Promise, "Async method with computed name"); + + assert.areEqual('h', obj.h().next().value, "Generator method with computed name"); + + assert.isTrue(obj.async() instanceof Promise, "Async method named async"); + assert.areEqual('async', obj2.async(), "Method named async"); + assert.areEqual('get async', obj3.async, "Getter named async"); + obj3.async = 123; + assert.areEqual('set async', out, "Setter named async"); + assert.areEqual('generator async', obj4.async().next().value, "Generator method named async"); + } + }, + { + name: "Uncommon object literal method name types", + body: function() { + var out = 'nothing'; + var obj = { + "s1"() { return "s1"; }, + async "s2"() { return "s2"; }, + * "s3"() { return "s3"; }, + get "s4"() { return "s4"; }, + set "s4"(v) { out = "s4"; }, + + 0.1() { return 0.1; }, + async 0.2() { return 0.2; }, + * 0.3() { return 0.3; }, + get 0.4() { return 0.4; }, + set 0.4(v) { out = 0.4; }, + + 123() { return 123; }, + async 456() { return 456; }, + * 789() { yield 789; }, + get 123456() { return 123456; }, + set 123456(v) { out = 123456; }, + + while() { return "while"; }, + async else() { return "else"; }, + * if() { return "if"; }, + get catch() { return "catch"; }, + set catch(v) { out = "catch"; }, + } + + assert.areEqual('s1', obj.s1(), "Method with string name"); + assert.areEqual(0.1, obj[0.1](), "Method with float name"); + assert.areEqual(123, obj[123](), "Method with numeric name"); + assert.areEqual('while', obj.while(), "Method with keyword name"); + + assert.isTrue(obj.s2() instanceof Promise, "Async method with string name"); + assert.isTrue(obj[0.2]() instanceof Promise, "Async method with float name"); + assert.isTrue(obj[456]() instanceof Promise, "Async method with numeric name"); + assert.isTrue(obj.else() instanceof Promise, "Async method with keyword name"); + + assert.areEqual('s3', obj.s3().next().value, "Generator method with string name"); + assert.areEqual(0.3, obj[0.3]().next().value, "Generator method with float name"); + assert.areEqual(789, obj[789]().next().value, "Generator method with numeric name"); + assert.areEqual('if', obj.if().next().value, "Generator method with keyword name"); + + assert.areEqual('s4', obj.s4, "Getter method with string name"); + assert.areEqual(0.4, obj[0.4], "Getter method with float name"); + assert.areEqual(123456, obj[123456], "Getter method with numeric name"); + assert.areEqual('catch', obj.catch, "Getter method with keyword name"); + + obj.s4 = 123 + assert.areEqual('s4', out, "Setter method with string name"); + obj[0.4] = 123 + assert.areEqual(0.4, out, "Setter method with float name"); + obj[123456] = 123 + assert.areEqual(123456, out, "Setter method with numeric name"); + obj.catch = 123 + assert.areEqual('catch', out, "Setter method with keyword name"); + } + }, + { + name: "Regular function nested in a deferred method", + body: function() { + var obj = { + m() { + function foo() { return 'foo'; } + return foo(); + } + } + + assert.areEqual('foo', obj.m(), "Regular function nested in a deferred method should not be parsed as a method"); + } + }, + { + name: "Method with 'super' capture", + body: function() { + var obj = { + m() { return () => super.bar(); } + } + Object.setPrototypeOf(obj, { bar() { return this; } }); + + assert.areEqual(obj, obj.m()(), "Method should call lambda should call super method should return this captured from obj.m"); + } + }, + { + name: "Async lambda with parens", + body: function() { + var a = async() => { }; + + assert.isTrue(a() instanceof Promise, "Async lambda with parens around formal parameters") + } + } +] + +testRunner.runTests(tests, { verbose: WScript.Arguments[0] != "summary" }); diff --git a/test/es6/rlexe.xml b/test/es6/rlexe.xml index 1d8587e2299..c1a4bebdf5f 100644 --- a/test/es6/rlexe.xml +++ b/test/es6/rlexe.xml @@ -1479,4 +1479,40 @@ BugFix + + + DeferParseLambda.js + -off:deferparse -args summary -endargs + + + + + DeferParseLambda.js + -off:deferparse -args summary -endargs -deferparse + + + + + DeferParseLambda.js + -off:deferparse -args summary -endargs -deferparse -forceundodefer + + + + + DeferParseMethods.js + -off:deferparse -args summary -endargs + + + + + DeferParseMethods.js + -off:deferparse -args summary -endargs -deferparse + + + + + DeferParseMethods.js + -off:deferparse -args summary -endargs -deferparse -forceundodefer + +