إنشاء تصميمات بناء جملة جافا سكريبت مخصصة باستخدام بابل. الجزء 1

ننشر اليوم الجزء الأول من ترجمة المادة ، المكرس لإنشاء تصميمات بناء الجملة الخاصة بك لجافا سكريبت باستخدام Babel.



نظرة عامة


أولاً ، دعونا نلقي نظرة على ما سنحققه عندما نصل إلى نهاية هذه المادة:

//  '@@'   `foo`   function @@ foo(a, b, c) {   return a + b + c; } console.log(foo(1, 2)(3)); // 6 

سنقوم بتنفيذ بناء الجملةالذي يسمح بوظائف currying . يشبه بناء الجملة هذا المستخدم في إنشاء وظائف المولد ، ولكن في حالتنا ، بدلاً من علامة * ، يتم وضع تسلسل منcharacter بين الكلمة الأساسية function واسم function . نتيجة لذلك ، عند تعريف الدالات ، يمكنك استخدام إنشاء function @@ name(arg1, arg2) النموذج function @@ name(arg1, arg2) .

في المثال أعلاه ، عند العمل مع الدالة foo ، يمكنك استخدام التطبيق الجزئي الخاص به. إن استدعاء الدالة foo خلال تمريرها العديد من المعلمات التي تقل عن عدد الوسائط التي تحتاجها ، سيعود بوظيفة جديدة يمكن أن تأخذ الوسيطات المتبقية:

 foo(1, 2, 3); // 6 const bar = foo(1, 2); // (n) => 1 + 2 + n bar(3); // 6 

اخترت تسلسل @@ لا يمكن استخدام الرمز @ في أسماء المتغيرات. هذا يعني أن إنشاء function@@foo(){} النموذج function@@foo(){} سيكون أيضًا صحيحًا من الناحية النحوية. بالإضافة إلى ذلك ، يتم استخدام "المشغل" @ لوظائف الديكور ، وأردت استخدام شيء جديد تمامًا. نتيجة لذلك ، اخترت البناء.

من أجل تحقيق هدفنا ، نحتاج إلى تنفيذ الإجراءات التالية:

  • إنشاء شوكة من محلل بابل.
  • قم بإنشاء بابل المساعد الخاص بك لتحويل التعليمات البرمجية.

يبدو وكأنه شيء مستحيل؟
في الواقع ، لا يوجد شيء فظيع هنا ، وسنحلل كل شيء بالتفصيل معًا. آمل أنك عندما تقرأ هذا ، ستتمكن من إتقان تعقيدات بابل.

صنع شوكة بابل


انتقل إلى مستودع بابل على جيثب وانقر على زر Fork ، الموجود في الجزء العلوي الأيسر من الصفحة.


خلق شوكة بابل ( الصورة بالحجم الكامل )

وبالمناسبة ، إذا أنشأت للتو شوكة المشروع المفتوح المصدر الشهير لأول مرة - تهانينا!

الآن استنساخ شوكة بابل على جهاز الكمبيوتر الخاص بك وإعداده للعمل .

 $ git clone https://github.com/tanhauhau/babel.git # set up $ cd babel $ make bootstrap $ make build 

اسمحوا لي الآن أن أتحدث بإيجاز عن تنظيم مستودع بابل.

بابل يستخدم monorepository. جميع الحزم (على سبيل المثال @babel/core و @babel/parser و @babel/plugin-transform-react-jsx وما إلى ذلك) موجودة في packages/ المجلد. يبدو مثل هذا:

 - doc - packages  - babel-core  - babel-parser  - babel-plugin-transform-react-jsx  - ... - Gulpfile.js - Makefile - ... 

ألاحظ أن بابل يستخدم Makefile لأتمتة المهام. عند إنشاء مشروع بواسطة أمر make build ، يتم استخدام Gulp كمدير المهام.

رمز التحويل إلى دورة AST القصيرة


إذا لم تكن على دراية بمفاهيم مثل "المحلل اللغوي" و "شجرة تركيب الخلاصة" (AST) ، ثم قبل أن تستمر في القراءة ، أوصي بشدة أن تلقي نظرة على هذه المادة.

إذا كنت تتحدث بإيجاز شديد عما يحدث عند تحليل (تحليل) الرمز ، فستحصل على ما يلي:

  • يبدو الرمز المقدم كسلسلة (سلسلة كتابة) قائمة طويلة من الأحرف: f, u, n, c, t, i, o, n, , @, @, f, ...
  • في البداية ، يقوم بابل بتنفيذ الرمز المميز. في هذه الخطوة ، يقوم بابل بمسح الرمز وإنشاء الرموز. على سبيل المثال ، شيء مثل function, @@, foo, (, a, ...
  • ثم يتم تمرير الرموز عبر المحلل اللغوي لتحليلها. هنا بابل ، استنادا إلى مواصفات لغة جافا سكريبت ، يخلق شجرة بناء جملة مجردة.

هنا مورد رائع لأولئك الذين يرغبون في معرفة المزيد عن المترجمين.

إذا كنت تعتقد أن "المترجم" هو شيء معقد للغاية وغير مفهوم ، فأعلم أن كل شيء في الواقع ليس غامضًا. التجميع هو ببساطة تحليل الرمز وإنشاء كود جديد على أساسه ، والذي سنطلق عليه XXX. يمكن تمثيل رمز XXX برمز الجهاز (ربما ، رمز الجهاز هو ما ينبثق أولاً في عقول معظمنا عندما نفكر في المترجم). قد يكون هذا رمز JavaScript متوافقًا مع المتصفحات القديمة. في الواقع ، واحدة من الوظائف الرئيسية لبابل هي تجميع كود JS الحديث في كود يمكن فهمه للمتصفحات القديمة.

تطوير محلل خاص بك لبابل


سوف نعمل في packages/babel-parser/ folder:

 - src/  - tokenizer/  - parser/  - plugins/    - jsx/    - typescript/    - flow/    - ... - test/ 

لقد تحدثنا بالفعل عن رمزية والتحليل. يمكنك العثور على التعليمات البرمجية التي تنفذ هذه العمليات في مجلدات مع أسماء المطابقة. تحتوي plugins/ المجلد على مكونات إضافية (مكونات إضافية) تعمل على توسيع إمكانات المحلل اللغوي الأساسي وإضافة دعم لبناء جملة إضافي في النظام. هذا هو بالضبط كيف ، على سبيل المثال ، يتم تنفيذ jsx ودعم flow .

دعنا نحل مشكلتنا باستخدام تقنية التطوير من خلال الاختبار (التطوير القائم على الاختبار ، TDD). في رأيي ، من الأسهل لكتابة اختبار أولاً ، ثم ، بالعمل تدريجياً على النظام ، قم بإجراء هذا الاختبار بدون أخطاء. هذا النهج جيد بشكل خاص عند العمل في قاعدة كود غير مألوفة. يجعل TDD من السهل فهم المكان الذي تحتاج فيه لإجراء تغييرات على الكود لتنفيذ وظيفتك المقصودة.

 packages/babel-parser/test/curry-function.js import { parse } from '../lib'; function getParser(code) {  return () => parse(code, { sourceType: 'module' }); } describe('curry function syntax', function() {  it('should parse', function() {    expect(getParser(`function @@ foo() {}`)()).toMatchSnapshot();  }); }); 

يمكنك تشغيل اختبار babel-parser مثل هذا: TEST_ONLY=babel-parser TEST_GREP="curry function" make test-only . سيسمح لك ذلك برؤية الأخطاء:

 SyntaxError: Unexpected token (1:9) at Parser.raise (packages/babel-parser/src/parser/location.js:39:63) at Parser.raise [as unexpected] (packages/babel-parser/src/parser/util.js:133:16) at Parser.unexpected [as parseIdentifierName] (packages/babel-parser/src/parser/expression.js:2090:18) at Parser.parseIdentifierName [as parseIdentifier] (packages/babel-parser/src/parser/expression.js:2052:23) at Parser.parseIdentifier (packages/babel-parser/src/parser/statement.js:1096:52) 

إذا وجدت أن عرض جميع الاختبارات يستغرق وقتًا طويلاً ، يمكنك ، لإجراء الاختبار المطلوب ، الاتصال بـ jest مباشرة:

 BABEL_ENV=test node_modules/.bin/jest -u packages/babel-parser/test/curry-function.js 

اكتشف المحلل اللغوي لدينا 2 @ الرموز ، على ما يبدو بريئة تمامًا ، حيث يجب ألا تكون.

كيف عرفت ذلك؟ ستساعدنا إجابة هذا السؤال في العثور على استخدام وضع مراقبة الكود الذي تم تشغيله بواسطة أمر make watch .

يؤدي عرض مكدس الاتصال إلى حزم / babel-parser / src / parser / expression.js ، حيث this.unexpected() طرح الاستثناء this.unexpected() .

أضف بضعة أوامر تسجيل إلى هذا الملف:

 packages/babel-parser/src/parser/expression.js parseIdentifierName(pos: number, liberal?: boolean): string {  if (this.match(tt.name)) {    // ...  } else {    console.log(this.state.type); //      console.log(this.lookahead().type); //      throw this.unexpected();  } } 

كما ترون ، كلا الرمزين @ :

 TokenType {  label: '@',  // ... } 

كيف اكتشفت أن الإنشاءات this.state.type و this.lookahead().type الرموز الحالية this.lookahead().type ؟
سأتحدث عن هذا في قسم هذه المادة المخصصة للوظائف this.eat و this.match و this.next .

قبل المتابعة ، دعونا نلخص:

  • كتبنا اختبارا babel-parser .
  • أجرينا الاختبار باستخدام make test-only .
  • استخدمنا وضع مراقبة الكود باستخدام make watch .
  • لقد تعلمنا عن حالة المحلل اللغوي this.state.type معلومات حول نوع الرمز المميز الحالي ( this.state.type ) في وحدة التحكم.

والآن سوف نتأكد من عدم اعتبار الأحرف 2 @ كرموز منفصلة ، ولكن كرمز جديد، الذي قررنا استخدامه لوظائف الكاري.

رمز جديد: ""


أولاً ، دعونا ننظر إلى حيث يتم تحديد أنواع الرموز. هذه هي حزم الملفات / babel-parser / src / tokenizer / types.js .

هنا يمكنك العثور على قائمة الرموز. أضف هنا تعريف رمز atat الجديد:

 packages/babel-parser/src/tokenizer/types.js export const types: { [name: string]: TokenType } = {  // ...  at: new TokenType('@'),  atat: new TokenType('@@'), }; 

الآن دعونا نبحث عن المكان في الكود حيث يتم إنشاء الرموز في عملية الرموز. يؤدي العثور على تسلسل أحرف tt.at في babel-parser/src/tokenizer إلى الملف: الحزم / babel-parser / src / tokenizer / index.js . في babel-parser يتم استيراد أنواع الرمز المميز كـ tt .

الآن ، إذا كان الرمز الحالي @ يأتي آخر @ ، tt.atat بإنشاء رمز مميز جديد tt.atat بدلاً من رمز tt.at :

 packages/babel-parser/src/tokenizer/index.js getTokenFromCode(code: number): void {  switch (code) {    // ...    case charCodes.atSign:      //    -  `@`      if (this.input.charCodeAt(this.state.pos + 1) === charCodes.atSign) {        //  `tt.atat`  `tt.at`        this.finishOp(tt.atat, 2);      } else {        this.finishOp(tt.at, 1);      }      return;    // ...  } } 

إذا قمت بإجراء الاختبار مرة أخرى ، ستلاحظ أن المعلومات حول الرموز الحالية والقادمة قد تغيرت:

 //   TokenType {  label: '@@',  // ... } //   TokenType {  label: 'name',  // ... } 

انها تبدو بالفعل جيدة جدا. سنواصل العمل.

محلل جديد


قبل الانتقال ، ألق نظرة على كيفية تمثيل وظائف المولد في AST.


AST لوظيفة المولد ( صورة بالحجم الكامل )

كما ترون ، يشير generator: true السمة generator: true لكيان FunctionDeclaration إلى أن هذه FunctionDeclaration منشئ.

يمكننا اتباع نهج مماثل لوصف وظيفة تدعم الكاري. وهي ، يمكننا إضافة curry: true السمة curry: true إلى FunctionDeclaration .


AST لوظيفة الكاري ( صورة بالحجم الكامل )

في الواقع ، الآن لدينا خطة. دعونا نتعامل مع تنفيذه.

إذا نظرت في الكود الخاص بكلمة FunctionDeclaration ، فيمكنك الانتقال إلى وظيفة parseFunction ، والتي يتم الإعلان عنها في الحزم / babel-parser / src / parser / statement.js . هنا يمكنك العثور على الخط الذي تم تعيين سمة generator . أضف سطرًا آخر إلى الكود:

 packages/babel-parser/src/parser/statement.js export default class StatementParser extends ExpressionParser {  // ...  parseFunction<T: N.NormalFunction>(    node: T,    statement?: number = FUNC_NO_FLAGS,    isAsync?: boolean = false  ): T {    // ...    node.generator = this.eat(tt.star);    node.curry = this.eat(tt.atat);  } } 

إذا أجرينا الاختبار مرة أخرى ، فستنتظرنا مفاجأة سارة. تم اختبار الرمز بنجاح!

 PASS packages/babel-parser/test/curry-function.js  curry function syntaxshould parse (12ms) 

هل هذا كل شيء؟ ماذا فعلنا لجعل الاختبار يمر بأعجوبة؟

لمعرفة ذلك ، دعنا نتحدث عن كيفية عمل التحليل. في سياق هذه المحادثة ، آمل أن تفهم كيف أن السطر node.curry = this.eat(tt.atat); .

أن تستمر ...

أعزائي القراء! هل تستخدم بابل؟


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


All Articles