Erstellen Sie mit Babel benutzerdefinierte JavaScript-Syntaxkonstrukte. Teil 2

Heute veröffentlichen wir den zweiten Teil einer Übersetzung der JavaScript-Syntaxerweiterung mit Babel.



→ Schwindelerregender erster Teil

Wie das Parsen funktioniert


Der Parser empfängt eine Liste von Token vom Code-Tokenisierungssystem und erstellt einen AST, indem er die Token einzeln untersucht. Um eine Entscheidung über die Verwendung von Token zu treffen und zu verstehen, welches Token als nächstes erwartet werden kann, verweist der Parser auf die Angabe der Grammatik der Sprache.

Die Grammatikspezifikation sieht ungefähr so ​​aus:

... ExponentiationExpression -> UnaryExpression                             UpdateExpression ** ExponentiationExpression MultiplicativeExpression -> ExponentiationExpression                             MultiplicativeExpression ("*" or "/" or "%") ExponentiationExpression AdditiveExpression    -> MultiplicativeExpression                             AdditiveExpression + MultiplicativeExpression                             AdditiveExpression - MultiplicativeExpression ... 

Es beschreibt die Priorität der Ausführung von Ausdrücken oder Anweisungen. Ein AdditiveExpression Ausdruck kann beispielsweise eines der folgenden Konstrukte darstellen:

  • Ausdruck MultiplicativeExpression .
  • Ein AdditiveExpression Ausdruck, gefolgt von einem + -Token-Operator, gefolgt von einem MultiplicativeExpression Ausdruck.
  • Ein AdditiveExpression Ausdruck, gefolgt von einem " - " - Token, gefolgt von einem MultiplicativeExpression Ausdruck.

Wenn wir also den Ausdruck 1 + 2 * 3 , sieht er folgendermaßen aus:

 (AdditiveExpression "+" 1 (MultiplicativeExpression "*" 2 3)) 

Aber es wird nicht so sein:

 (MultiplicativeExpression "*" (AdditiveExpression "+" 1 2) 3) 

Das Programm wird unter Verwendung dieser Regeln in vom Parser ausgegebenen Code konvertiert:

 class Parser {  // ...  parseAdditiveExpression() {    const left = this.parseMultiplicativeExpression();    //    -  `+`  `-`    if (this.match(tt.plus) || this.match(tt.minus)) {      const operator = this.state.type;      //          this.nextToken();      const right = this.parseMultiplicativeExpression();      //        this.finishNode(        {          operator,          left,          right,        },        'BinaryExpression'      );    } else {      //  MultiplicativeExpression      return left;    }  } } 

Bitte beachten Sie, dass hier eine extrem vereinfachte Version dessen ist, was tatsächlich in Babel vorhanden ist. Ich hoffe jedoch, dass dieser Code es uns ermöglicht, die Essenz des Geschehens zu veranschaulichen.

Wie Sie sehen können, ist der Parser von Natur aus rekursiv. Es wechselt von den Designs mit der niedrigsten Priorität zu den Designs mit der höchsten Priorität. Beispielsweise ruft parseMultiplicativeExpression , und dieses Konstrukt ruft parseExponentiationExpression usw. auf. Dieser rekursive Prozess wird als rekursives Abstiegsparsing bezeichnet .

Funktionen this.eat, this.match, this.next


Möglicherweise haben Sie bemerkt, dass in den vorherigen Beispielen einige Hilfsfunktionen verwendet wurden, z. B. this.eat , this.match , this.next und andere. Dies sind die internen Funktionen des Babel-Parsers. Diese Funktionen sind jedoch nicht nur für Babel verfügbar, sondern normalerweise auch in anderen Parsern vorhanden.

  • Die Funktion this.match gibt einen booleschen Wert zurück, der angibt, ob das aktuelle Token die angegebene Bedingung erfüllt.
  • Die Funktion this.next sich in der Liste der Token vorwärts zum nächsten Token.
  • Die Funktion this.eat gibt dasselbe zurück wie die Funktion this.match Wenn this.match true this.eat führt this.eat vor der Rückgabe von true einen Aufruf von this.next .
  • Mit der Funktion this.lookahead können Sie das nächste Token this.lookahead ohne vorwärts zu gehen. this.lookahead hilft Ihnen, eine Entscheidung über den aktuellen Knoten zu treffen.

Wenn Sie sich den von uns geänderten Parser-Code noch einmal ansehen, werden Sie feststellen, dass das Lesen viel einfacher geworden ist:

 packages/babel-parser/src/parser/statement.js export default class StatementParser extends ExpressionParser {  parseStatementContent(/* ...*/) {    // ...    // NOTE:   match        if (this.match(tt._function)) {      this.next();      // NOTE:     ,          this.parseFunction();    }  }  // ...  parseFunction(/* ... */) {    // NOTE:   eat         node.generator = this.eat(tt.star);    node.curry = this.eat(tt.atat);    node.id = this.parseFunctionId();  } } 

Ich weiß, dass ich mich nicht eingehend mit den Funktionen von Parsern befasst habe. Deshalb hier und da - ein paar nützliche Ressourcen zu diesem Thema. Ich habe viele davon gelernt und kann sie Ihnen empfehlen.

Vielleicht möchten Sie wissen, wie ich die in Babel AST Explorer erstellte Syntax visualisieren konnte, als ich das neue curry Attribut in AST zeigte.

Dies wurde möglich, weil ich im Babel AST Explorer eine neue Funktion hinzugefügt habe, mit der Sie Ihren eigenen Parser in dieses AST-Recherchetool laden können.

Wenn Sie dem Pfad packages/babel-parser/lib , finden Sie eine kompilierte Version des Parsers und eine Codezuordnung. Im Babel AST Explorer Bedienfeld sehen Sie die Schaltfläche zum Laden Ihres eigenen Parsers. Durch Herunterladen der packages/babel-parser/lib/index.js Sie den mit Ihrem eigenen Parser generierten AST visualisieren.


AST-Visualisierung

Unser Plugin für Babel


Nachdem der Parser vollständig ist, schreiben wir ein Plugin für Babel.

Aber vielleicht haben Sie jetzt einige Zweifel, wie genau wir unseren eigenen Babel-Parser verwenden werden, insbesondere wenn man bedenkt, welchen Technologie-Stack wir zum Erstellen des Projekts verwenden.

Es gibt zwar nichts zu befürchten. Das Babel-Plugin kann Parser-Funktionen bereitstellen. Zugehörige Dokumentationen finden Sie auf der Babel-Website.

 babel-plugin-transformation-curry-function.js import customParser from './custom-parser'; export default function ourBabelPlugin() {  return {    parserOverride(code, opts) {      return customParser.parse(code, opts);    },  }; } 

Da wir einen Fork des Babel-Parsers erstellt haben, funktionieren alle vorhandenen Parser-Funktionen sowie die integrierten Plugins weiterhin einwandfrei.

Nachdem wir diese Zweifel beseitigt haben, schauen wir uns an, wie eine Funktion so erstellt wird, dass sie das Curry unterstützt.

Wenn Sie die Erwartungen nicht erfüllen konnten und bereits versucht haben, unser Plug-In zu Ihrem Projekterstellungssystem hinzuzufügen, werden Sie möglicherweise feststellen, dass Funktionen, die das Currying unterstützen, zu regulären Funktionen kompiliert werden.

Dies geschieht, weil Babel nach dem Parsen und Transformieren des Codes @babel/generator , um Code aus dem transformierten AST zu generieren. Da @babel/generator nichts über das neue curry Attribut weiß, wird es einfach ignoriert.

Wenn eines Tages Funktionen, die das Currying unterstützen, in den JavaScript-Standard aufgenommen werden, möchten Sie möglicherweise eine PR durchführen, um hier neuen Code hinzuzufügen.

Damit die Funktion das Currying unterstützt, können Sie sie in ein Funktionscurrying höherer Ordnung einwickeln:

 function currying(fn) {  const numParamsRequired = fn.length;  function curryFactory(params) {    return function (...args) {      const newParams = params.concat(args);      if (newParams.length >= numParamsRequired) {        return fn(...newParams);      }      return curryFactory(newParams);    }  }  return curryFactory([]); } 

Wenn Sie an den Funktionen der Implementierung des Mechanismus der Currying-Funktionen in JS interessiert sind, schauen Sie sich dieses Material an.

Infolgedessen können wir eine Funktion transformieren, die das Currying unterstützt:

 //   function @@ foo(a, b, c) {  return a + b + c; } //    const foo = currying(function foo(a, b, c) {  return a + b + c; }) 

Im Moment werden wir uns nicht mit dem Mechanismus zum Auslösen von Funktionen in JavaScript befassen, mit dem Sie die Funktion foo aufrufen können, bevor sie definiert ist.

So sieht der Transformationscode aus:

 babel-plugin-transformation-curry-function.js export default function ourBabelPlugin() {  return {    // ... <i>    visitor: {      FunctionDeclaration(path) {        if (path.get('curry').node) {          // const foo = curry(function () { ... });          path.node.curry = false;          path.replaceWith(            t.variableDeclaration('const', [              t.variableDeclarator(                t.identifier(path.get('id.name').node),                t.callExpression(t.identifier('currying'), [                  t.toExpression(path.node),                ])              ),            ])          );        }      },    },</i>  }; } 

Es wird für Sie viel einfacher sein, es herauszufinden, wenn Sie dieses Material über Transformationen in Babel lesen.

Nun stehen wir vor der Frage, wie dieser Mechanismus Zugang zur currying Funktion erhalten kann. Hier können Sie einen von zwei Ansätzen verwenden.

▍Ansatz Nr. 1: Es kann davon ausgegangen werden, dass die Currying-Funktion im globalen Bereich deklariert ist


Wenn ja, ist die Arbeit bereits erledigt.

Wenn sich beim Ausführen des kompilierten Codes herausstellt, dass die currying Funktion nicht definiert ist, wird eine Fehlermeldung angezeigt, die wie currying is not defined aussieht: " currying is not defined ". Es ist der Meldung " regeneratorRuntime is not defined " sehr ähnlich.

Wenn jemand Ihr babel-plugin-transformation-curry-function , müssen Sie ihn möglicherweise darüber informieren, dass er die currying Polyfüllung installieren muss, um sicherzustellen, dass dieses Plugin ordnungsgemäß funktioniert.

▍ Ansatz 2: Sie können Babel / Helfer verwenden


Sie können @babel/helpers eine neue Hilfsfunktion hinzufügen. Es ist unwahrscheinlich, dass diese Entwicklung mit dem offiziellen @babel/helpers kombiniert wird. Infolgedessen müssen Sie einen Weg finden, um @babel/core den Speicherort Ihres @babel/helpers :

 package.json {  "resolutions": {    "@babel/helpers": "7.6.0--your-custom-forked-version",  } 

Ich habe es selbst nicht versucht, aber ich glaube, dass dieser Mechanismus funktionieren wird. Wenn Sie es versuchen und auf Probleme stoßen, werde ich es gerne besprechen .

@babel/helpers neuen Hilfsfunktion zu @babel/helpers sehr einfach.

Wechseln Sie zunächst zur Datei packages / babel-helpers / src / helpers.js und fügen Sie einen neuen Eintrag hinzu:

 helpers.currying = helper("7.6.0")`  export default function currying(fn) {    const numParamsRequired = fn.length;    function curryFactory(params) {      return function (...args) {        const newParams = params.concat(args);        if (newParams.length >= numParamsRequired) {          return fn(...newParams);        }        return curryFactory(newParams);      }    }    return curryFactory([]);  } `; 

Bei der Beschreibung einer Hilfsfunktion wird die erforderliche Version @babel/core angegeben. Einige Schwierigkeiten können hier durch den export default der currying Funktion verursacht werden.

Um eine this.addHelper() zu verwenden, rufen this.addHelper() einfach this.addHelper() :

 // ... path.replaceWith(  t.variableDeclaration('const', [    t.variableDeclarator(      t.identifier(path.get('id.name').node),      t.callExpression(this.addHelper("currying"), [        t.toExpression(path.node),      ])    ),  ]) ); 

Der Befehl this.addHelper bei Bedarf die this.addHelper oben in die Datei ein und gibt einen Identifier , der die implementierte Funktion angibt.

Anmerkungen


Ich bin schon seit einiger Zeit an der Arbeit an Babel beteiligt, musste jedoch noch keine Funktionen hinzufügen, um die neue JavaScript-Syntax für den Parser zu unterstützen. Ich habe hauptsächlich daran gearbeitet, Fehler zu beheben und die für die offiziellen Sprachfunktionen relevanten Funktionen zu verbessern.

Seit einiger Zeit beschäftigte ich mich jedoch mit der Idee, der Sprache neue Syntaxkonstrukte hinzuzufügen. Aus diesem Grund habe ich beschlossen, Material darüber zu schreiben und es auszuprobieren. Es ist unglaublich schön zu sehen, dass alles genau wie erwartet funktioniert.

Die Möglichkeit, die Syntax der von Ihnen verwendeten Sprache zu steuern, ist eine starke Inspirationsquelle. Dies ermöglicht es, durch die Implementierung einiger komplexer Konstruktionen weniger Code zu schreiben oder einfacheren Code als zuvor zu schreiben. Die Mechanismen zur Umwandlung von einfachem Code in komplexe Konstruktionen werden automatisiert und in die Kompilierungsphase übertragen. Dies erinnert daran, wie async/await die Probleme von Höllenrückrufen und langen Versprechungsketten löst.

Zusammenfassung


Hier haben wir darüber gesprochen, wie die Funktionen des Babel-Parsers geändert werden können. Wir haben unser eigenes Code-Transformations-Plugin geschrieben, kurz über @babel/generator und über das Erstellen von Hilfsfunktionen mit @babel/helpers . Informationen zur Transformation des Codes werden nur schematisch gegeben. Lesen Sie hier mehr darüber.

Dabei haben wir einige Funktionen der Parser angesprochen. Wenn Sie sich für dieses Thema interessieren - dann hier und da - Ressourcen, die für Sie nützlich sind.

Die Reihenfolge der Aktionen, die wir ausgeführt haben, ist einem Teil des Prozesses sehr ähnlich, der ausgeführt wird, wenn eine neue JavaScript-Funktion an TC39 gesendet wird. Auf der TC39-Repository-Seite finden Sie Informationen zu aktuellen Angeboten. Hier finden Sie detailliertere Informationen zur Arbeit mit ähnlichen Angeboten. Wenn Sie eine neue JavaScript-Funktion vorschlagen, schreibt derjenige, der sie anbietet, normalerweise Polyfills oder bereitet durch Verzweigen von Babel eine Demonstration vor, die beweist, dass der Satz funktioniert. Wie Sie sehen, ist das Erstellen einer Abzweigung eines Parsers oder das Schreiben einer Polyfüllung nicht der schwierigste Teil des Prozesses zum Vorschlagen neuer JS-Funktionen. Es ist schwierig, den Themenbereich Innovation zu bestimmen, Optionen für seine Verwendung und Grenzfälle zu planen und zu überdenken. Es ist schwierig, die Meinungen und Vorschläge von Mitgliedern der JavaScript-Programmierer-Community zu sammeln Daher möchte ich all jenen meinen Dank aussprechen, die die Kraft finden, TC39 neue JavaScript-Funktionen anzubieten und damit diese Sprache zu entwickeln.

Hier ist eine Seite auf GitHub, auf der Sie einen Überblick über das bekommen, was wir hier gemacht haben.

Liebe Leser! Wollten Sie schon immer die Syntax von JavaScript erweitern?


Source: https://habr.com/ru/post/de470878/


All Articles