Das ECMAScript-Spezifikation verstehen, Teil 3
In dieser Episode vertiefen wir uns in die Definition der ECMAScript-Sprache und ihrer Syntax. Falls Sie mit kontextfreien Grammatiken nicht vertraut sind, ist jetzt ein guter Zeitpunkt, die Grundlagen zu überprüfen, da die Spezifikation kontextfreie Grammatiken zur Definition der Sprache verwendet. Sehen Sie das Kapitel über kontextfreie Grammatiken in "Crafting Interpreters" für eine zugängliche Einführung oder die Wikipedia-Seite für eine mathematischere Definition.
ECMAScript-Grammatiken
Die ECMAScript-Spezifikation definiert vier Grammatiken:
Die lexikalische Grammatik beschreibt, wie Unicode-Codepunkte in eine Sequenz von Eingabeelementen (Tokens, Zeilenenden, Kommentare, Leerzeichen) übersetzt werden.
Die syntaktische Grammatik definiert, wie syntaktisch korrekte Programme aus Tokens zusammengesetzt sind.
Die RegExp-Grammatik beschreibt, wie Unicode-Codepunkte in reguläre Ausdrücke übersetzt werden.
Die numerische Zeichenketten-Grammatik beschreibt, wie Zeichenketten in numerische Werte übersetzt werden.
Jede Grammatik wird als kontextfreie Grammatik definiert, bestehend aus einer Reihe von Produktionsregeln.
Die Grammatiken verwenden leicht unterschiedliche Notationen: Die syntaktische Grammatik verwendet LeftHandSideSymbol :
, während die lexikalische Grammatik und die RegExp-Grammatik LeftHandSideSymbol ::
und die numerische Zeichenketten-Grammatik LeftHandSideSymbol :::
verwenden.
Als nächstes betrachten wir die lexikalische Grammatik und die syntaktische Grammatik genauer.
Lexikalische Grammatik
Die Spezifikation definiert ECMAScript-Quelltext als eine Sequenz von Unicode-Codepunkten. Zum Beispiel sind Variablennamen nicht auf ASCII-Zeichen beschränkt, sondern können auch andere Unicode-Zeichen enthalten. Die Spezifikation spricht nicht über die eigentliche Kodierung (z. B. UTF-8 oder UTF-16). Es wird angenommen, dass der Quellcode bereits in eine Sequenz von Unicode-Codepunkten umgewandelt wurde, entsprechend der Kodierung, in der er vorlag.
Es ist nicht möglich, ECMAScript-Quellcode im Voraus zu tokenisieren, was die Definition der lexikalischen Grammatik etwas komplizierter macht.
Zum Beispiel können wir nicht feststellen, ob /
der Divisionsoperator oder der Anfang eines RegExps ist, ohne den größeren Kontext zu betrachten, in dem es auftritt:
const x = 10 / 5;
Hier ist /
ein DivPunctuator
.
const r = /foo/;
Hier ist das erste /
der Anfang eines RegularExpressionLiteral
.
Templates führen eine ähnliche Mehrdeutigkeit ein — die Interpretation von }`
hängt vom Kontext ab, in dem es auftritt:
const what1 = 'temp';
const what2 = 'late';
const t = `I am a ${ what1 + what2 }`;
Hier ist `I am a ${
ein TemplateHead
und }`
ein TemplateTail
.
if (0 == 1) {
}`not very useful`;
Hier ist }
ein RightBracePunctuator
und `
der Anfang eines NoSubstitutionTemplate
.
Auch wenn die Interpretation von /
und }`
von ihrem „Kontext“ — ihrer Position in der syntaktischen Struktur des Codes — abhängt, sind die Grammatiken, die wir als Nächstes beschreiben, dennoch kontextfrei.
Die lexikalische Grammatik verwendet mehrere Zielsymbole, um zwischen den Kontexten zu unterscheiden, in denen einige Eingabeelemente erlaubt sind und andere nicht. Zum Beispiel wird das Zielsymbol InputElementDiv
in Kontexten verwendet, in denen /
eine Division und /=
eine Divisionszuweisung ist. Die InputElementDiv
-Produktionsregeln listen die möglichen Tokens auf, die in diesem Kontext erzeugt werden können:
InputElementDiv ::
WhiteSpace
LineTerminator
Comment
CommonToken
DivPunctuator
RightBracePunctuator
In diesem Kontext führt das Auftreten von /
zum Eingabeelement DivPunctuator
. Hier ist es nicht möglich, ein RegularExpressionLiteral
zu erzeugen.
Andererseits ist InputElementRegExp
das Zielsymbol für die Kontexte, in denen /
der Anfang eines RegExps ist:
InputElementRegExp ::
WhiteSpace
LineTerminator
Comment
CommonToken
RightBracePunctuator
RegularExpressionLiteral
Wie wir aus den Produktionen sehen, kann dies das Eingabeelement RegularExpressionLiteral
erzeugen, aber das Erzeugen eines DivPunctuator
ist hier nicht möglich.
Ähnlich gibt es ein weiteres Zielsymbol, InputElementRegExpOrTemplateTail
, für Kontexte, in denen TemplateMiddle
und TemplateTail
zusätzlich zu RegularExpressionLiteral
erlaubt sind. Schließlich ist InputElementTemplateTail
das Zielsymbol für Kontexte, in denen nur TemplateMiddle
und TemplateTail
erlaubt sind, aber RegularExpressionLiteral
nicht erlaubt ist.
In Implementierungen kann der syntaktische Grammatikanalysator („Parser“) den lexikalischen Grammatikanalysator („Tokenizer“ oder „Lexer“) aufrufen, das Zielsymbol als Parameter übergeben und nach dem nächsten Eingabeelement fragen, das für dieses Zielsymbol geeignet ist.
Syntaktische Grammatik
Wir haben die lexikalische Grammatik untersucht, die definiert, wie wir Token aus Unicode-Codepunkten konstruieren. Die syntaktische Grammatik baut darauf auf: Sie definiert, wie syntaktisch korrekte Programme aus Token zusammengesetzt sind.
Beispiel: Zulassen von Legacy-Bezeichnern
Das Einführen eines neuen Schlüsselworts in die Grammatik ist möglicherweise eine Änderung, die zu Kompatibilitätsproblemen führen kann — was passiert, wenn bestehender Code das Schlüsselwort bereits als Bezeichner verwendet?
Zum Beispiel könnte jemand, bevor await
ein Schlüsselwort war, den folgenden Code geschrieben haben:
function old() {
var await;
}
Die ECMAScript-Grammatik hat das Schlüsselwort await
so vorsichtig hinzugefügt, dass dieser Code weiterhin funktioniert. Innerhalb von asynchronen Funktionen ist await
ein Schlüsselwort, sodass dies nicht funktioniert:
async function modern() {
var await; // Syntaxfehler
}
Das Zulassen von yield
als Bezeichner in Nicht-Generatoren und das Verbot in Generatoren funktioniert ähnlich.
Das Verständnis, wie await
als Bezeichner erlaubt ist, erfordert das Verständnis der ECMAScript-spezifischen syntaktischen Grammatiknotation. Tauchen wir direkt ein!
Produktionen und Kurzschreibweise
Schauen wir uns an, wie die Produktionen für VariableStatement
definiert sind. Auf den ersten Blick kann die Grammatik etwas einschüchternd wirken:
VariableStatement[Yield, Await] :
var VariableDeclarationList[+In, ?Yield, ?Await] ;
Was bedeuten die Indizes ([Yield, Await]
) und Präfixe (+
in +In
und ?
in ?Async
)?
Die Notation wird im Abschnitt Grammar Notation erklärt.
Die Indizes sind eine Kurzschreibweise, um eine Gruppe von Produktionen, für eine Gruppe von linken Symbolen, gleichzeitig auszudrücken. Das linke Symbol hat zwei Parameter, die sich in vier „echte“ linke Symbole erweitern: VariableStatement
, VariableStatement_Yield
, VariableStatement_Await
und VariableStatement_Yield_Await
.
Beachten Sie, dass hier das einfache VariableStatement
„VariableStatement
ohne _Await
und _Yield
“ bedeutet. Es sollte nicht mit VariableStatement[Yield, Await]
verwechselt werden.
Auf der rechten Seite der Produktion sehen wir die Kurzschreibweise +In
, was bedeutet „verwende die Version mit _In
“, und ?Await
, was bedeutet „verwende die Version mit _Await
, wenn und nur wenn das linke Symbol _Await
hat“ (ähnlich mit ?Yield
).
Die dritte Kurzschreibweise, ~Foo
, was bedeutet „verwende die Version ohne _Foo
“, wird in dieser Produktion nicht verwendet.
Mit diesen Informationen können wir die Produktionen wie folgt erweitern:
VariableStatement :
var VariableDeclarationList_In ;
VariableStatement_Yield :
var VariableDeclarationList_In_Yield ;
VariableStatement_Await :
var VariableDeclarationList_In_Await ;
VariableStatement_Yield_Await :
var VariableDeclarationList_In_Yield_Await ;
Letztendlich müssen wir zwei Dinge herausfinden:
- Wo wird entschieden, ob wir uns im Fall mit
_Await
oder ohne_Await
befinden? - Wo macht es einen Unterschied — wo unterscheiden sich die Produktionen für
Something_Await
undSomething
(ohne_Await
)?
_Await
oder kein _Await
?
Lassen Sie uns zuerst Frage 1 angehen. Es ist einigermaßen leicht zu erraten, dass Nicht-Async-Funktionen und Async-Funktionen sich darin unterscheiden, ob wir den Parameter _Await
für den Funktionskörper wählen oder nicht. Wenn wir die Produktionen für asynchrone Funktionsdeklarationen lesen, finden wir dieses:
AsyncFunctionBody :
FunctionBody[~Yield, +Await]
Beachten Sie, dass AsyncFunctionBody
keine Parameter hat — diese werden dem FunctionBody
auf der rechten Seite hinzugefügt.
Wenn wir diese Produktion erweitern, erhalten wir:
AsyncFunctionBody :
FunctionBody_Await
Mit anderen Worten haben asynchrone Funktionen FunctionBody_Await
, was bedeutet, dass der Funktionskörper await
als Schlüsselwort behandelt.
Andererseits, wenn wir uns innerhalb einer Nicht-Async-Funktion befinden, ist die relevante Produktion:
FunctionDeclaration[Yield, Await, Default] :
function BindingIdentifier[?Yield, ?Await] ( FormalParameters[~Yield, ~Await] ) { FunctionBody[~Yield, ~Await] }
(FunctionDeclaration
hat eine weitere Produktion, aber diese ist für unser Beispiel nicht relevant.)
Um eine kombinatorische Erweiterung zu vermeiden, lassen Sie uns den Parameter Default
ignorieren, der in dieser speziellen Produktion nicht verwendet wird.
Die erweiterte Form der Produktion lautet:
FunctionDeclaration :
function BindingIdentifier ( FormalParameters ) { FunctionBody }
FunctionDeclaration_Yield :
function BindingIdentifier_Yield ( FormalParameters ) { FunctionBody }
FunctionDeclaration_Await :
function BindingIdentifier_Await ( FormalParameters ) { FunctionBody }
FunctionDeclaration_Yield_Await :
function BindingIdentifier_Yield_Await ( FormalParameters ) { FunctionBody }
In dieser Produktion erhalten wir immer FunctionBody
und FormalParameters
(ohne _Yield
und ohne _Await
), da sie mit [~Yield, ~Await]
in der nicht erweiterten Produktion parametriert sind.
Der Funktionsname wird unterschiedlich behandelt: Er erhält die Parameter _Await
und _Yield
, wenn das Symbol auf der linken Seite diese hat.
Zusammenfassend: Asynchrone Funktionen haben einen FunctionBody_Await
und nicht asynchrone Funktionen haben einen FunctionBody
(ohne _Await
). Da wir über nicht-generative Funktionen sprechen, sind sowohl unsere asynchrone Beispiel-Funktion als auch unsere nicht asynchrone Beispiel-Funktion ohne _Yield
parametriert.
Vielleicht ist es schwierig, sich zu merken, welcher FunctionBody
und welcher FunctionBody_Await
ist. Ist FunctionBody_Await
für eine Funktion, bei der await
ein Bezeichner ist, oder für eine Funktion, bei der await
ein Schlüsselwort ist?
Man kann den _Await
-Parameter so denken, dass er "await
ist ein Schlüsselwort" bedeutet. Dieser Ansatz ist auch zukunftssicher. Stellen Sie sich ein neues Schlüsselwort, blob
, vor, das hinzugefügt wird, aber nur innerhalb von "blob-artigen" Funktionen. Nicht blob-artige, nicht asynchrone, nicht generatorartige Funktionen hätten noch FunctionBody
(ohne _Await
, _Yield
oder _Blob
), genau wie jetzt. Blob-artige Funktionen hätten FunctionBody_Blob
, asynchrone blob-artige Funktionen hätten FunctionBody_Await_Blob
und so weiter. Wir müssten noch Blob
zu den Produktionen hinzufügen, aber die erweiterten Formen von FunctionBody
für bereits existierende Funktionen bleiben gleich.
await
als Bezeichner verbieten
Als Nächstes müssen wir herausfinden, wie await
als Bezeichner ausgeschlossen wird, wenn wir uns innerhalb eines FunctionBody_Await
befinden.
Wir können die Produktionen weiter verfolgen, um zu sehen, dass der _Await
-Parameter unverändert von FunctionBody
bis zur VariableStatement
-Produktion weitergegeben wird, die wir zuvor untersucht haben.
Folglich haben wir innerhalb einer asynchronen Funktion eine VariableStatement_Await
und innerhalb einer nicht asynchronen Funktion eine VariableStatement
.
Wir können die Produktionen weiter verfolgen und die Parameter im Auge behalten. Wir haben bereits die Produktionen für VariableStatement
gesehen:
VariableStatement[Yield, Await] :
var VariableDeclarationList[+In, ?Yield, ?Await] ;
Alle Produktionen für VariableDeclarationList
geben die Parameter einfach so weiter:
VariableDeclarationList[In, Yield, Await] :
VariableDeclaration[?In, ?Yield, ?Await]
(Hier zeigen wir nur die Produktion, die für unser Beispiel relevant ist.)
VariableDeclaration[In, Yield, Await] :
BindingIdentifier[?Yield, ?Await] Initializer[?In, ?Yield, ?Await] opt
Die opt
-Kurzschrift bedeutet, dass das rechte Symbol optional ist; es gibt tatsächlich zwei Produktionen, eine mit dem optionalen Symbol und eine ohne.
Im einfachen Fall, der für unser Beispiel relevant ist, besteht VariableStatement
aus dem Schlüsselwort var
, gefolgt von einem einzelnen BindingIdentifier
ohne Initialisierer und endet mit einem Semikolon.
Um await
als BindingIdentifier
zu erlauben oder zu verbieten, hoffen wir auf etwas wie dies:
BindingIdentifier_Await :
Identifier
yield
BindingIdentifier :
Identifier
yield
await
Dies würde await
als einen Bezeichner innerhalb asynchroner Funktionen verbieten und es innerhalb nicht asynchroner Funktionen erlauben.
Aber die Spezifikation definiert es nicht so; stattdessen finden wir diese Produktion:
BindingIdentifier[Yield, Await] :
Identifier
yield
await
Expansion bedeutet folgendes Produktionen:
BindingIdentifier_Await :
Identifier
yield
await
BindingIdentifier :
Identifier
yield
await
(Wir lassen die Produktionen für BindingIdentifier_Yield
und BindingIdentifier_Yield_Await
weg, die in unserem Beispiel nicht benötigt werden.)
Dies sieht so aus, als ob await
und yield
immer als Bezeichner erlaubt wären. Was ist los damit? Ist der ganze Blogpost umsonst?
Statische Semantik zur Rettung
Es stellt sich heraus, dass statische Semantik benötigt werden, um await
als Bezeichner innerhalb asynchroner Funktionen zu verbieten.
Statische Semantik beschreiben statische Regeln – das sind Regeln, die vor dem Lauf des Programms geprüft werden.
In diesem Fall definieren die statische Semantik für BindingIdentifier
die folgende syntaxgesteuerte Regel:
BindingIdentifier[Yield, Await] : await
Es ist ein Syntaxfehler, wenn diese Produktion einen
[Await]
-Parameter hat.
Effektiv verbietet dies die BindingIdentifier_Await : await
Produktion.
Die Spezifikation erklärt, dass der Grund für diese Produktion, die jedoch durch die statischen Semantiken als Syntaxfehler definiert wird, in der Interferenz mit der automatischen Semikolon-Einfügung (ASI) liegt.
Denken Sie daran, dass ASI einsetzt, wenn wir eine Codezeile anhand der Grammatikproduktionen nicht analysieren können. ASI versucht, Semikola hinzuzufügen, um die Anforderung zu erfüllen, dass Anweisungen und Deklarationen mit einem Semikolon enden müssen. (Wir werden ASI in einer späteren Episode ausführlicher beschreiben.)
Betrachten Sie den folgenden Code (Beispiel aus der Spezifikation):
async function too_few_semicolons() {
let
await 0;
}
Wenn die Grammatik await
als Bezeichner verbieten würde, würde ASI eingreifen und den Code in den folgenden grammatikalisch korrekten Code umwandeln, der auch let
als Bezeichner verwendet:
async function too_few_semicolons() {
let;
await 0;
}
Diese Art der Interferenz mit ASI wurde als zu verwirrend empfunden, daher wurden statische Semantiken verwendet, um await
als Bezeichner zu verbieten.
Verbotene StringValues
von Bezeichnern
Es gibt auch eine andere verwandte Regel:
BindingIdentifier : Identifier
Es ist ein Syntaxfehler, wenn diese Produktion einen
[Await]
-Parameter hat und derStringValue
desIdentifier
"await"
ist.
Dies könnte anfangs verwirrend sein. Identifier
ist wie folgt definiert:
Identifier :
IdentifierName aber nicht ReservedWord
await
ist ein ReservedWord
, wie kann ein Identifier
jemals await
sein?
Wie sich herausstellt, kann ein Identifier
nicht await
sein, aber es kann etwas anderes sein, dessen StringValue
"await"
ist — eine andere Darstellung der Zeichenfolge await
.
Statische Semantiken für Bezeichnernamen definieren, wie der StringValue
eines Bezeichnernamens berechnet wird. Zum Beispiel ist die Unicode-Escape-Sequenz für a
\u0061
, daher hat \u0061wait
den StringValue
"await"
. \u0061wait
wird von der lexikalischen Grammatik nicht als Schlüsselwort erkannt, sondern stattdessen als Identifier
. Die statischen Semantiken verbieten dessen Verwendung als Variablenname in asynchronen Funktionen.
Das funktioniert also:
function old() {
var \u0061wait;
}
Und das funktioniert nicht:
async function modern() {
var \u0061wait; // Syntaxfehler
}
Zusammenfassung
In dieser Episode haben wir uns mit der lexikalischen Grammatik, der syntaktischen Grammatik und den Abkürzungen, die bei der Definition der syntaktischen Grammatik verwendet werden, vertraut gemacht. Als Beispiel haben wir untersucht, wie await
als Bezeichner in asynchronen Funktionen verboten wird, aber in nicht-asynchronen Funktionen erlaubt bleibt.
Andere interessante Teile der syntaktischen Grammatik, wie die automatische Semikolon-Einfügung und Cover-Grammatiken, werden in einer späteren Episode behandelt. Bleiben Sie dran!