Eval of Julia code

أحد أصعب الأجزاء في تعلم كيفية تشغيل لغة جوليا للكود هو تعلم كيفية عمل جميع الأجزاء معًا لتنفيذ كتلة من الكود.

كل جزء من الكود عادة ما يمر بعدة خطوات بأسماء قد تكون غير مألوفة، مثل (دون ترتيب معين): flisp، AST، C++، LLVM، eval، typeinf، macroexpand، sysimg (أو صورة النظام)، التمهيد، التجميع، التحليل، التنفيذ، JIT، التفسير، التعبئة، التفريغ، الدالة الداخلية، والدالة الأولية، قبل أن يتحول إلى النتيجة المرغوبة (نأمل).

Definitions
  • REPL

    REPL تعني حلقة القراءة والتقييم والطباعة. إنها فقط ما نسميه بيئة سطر الأوامر باختصار.

  • AST

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

مخطط تدفق المترجم

Julia Execution

وجهة نظر 10,000 قدم من العملية الكاملة هي كما يلي:

  1. يبدأ المستخدم julia.
  2. تتم استدعاء دالة C main() من cli/loader_exe.c. تقوم هذه الدالة بمعالجة وسائط سطر الأوامر، مما يملأ هيكل jl_options ويضبط المتغير ARGS. ثم تقوم بتهيئة جوليا (عن طريق استدعاء julia_init in init.c، والذي قد يقوم بتحميل sysimg الذي تم تجميعه مسبقًا. أخيرًا، تقوم بنقل التحكم إلى جوليا عن طريق استدعاء Base._start().
  3. عندما تتولى _start() السيطرة، تعتمد سلسلة الأوامر اللاحقة على وسائط سطر الأوامر المقدمة. على سبيل المثال، إذا تم تزويد اسم ملف، فسوف تتابع لتنفيذ ذلك الملف. خلاف ذلك، ستبدأ في واجهة تفاعلية REPL.
  4. تخطي التفاصيل حول كيفية تفاعل REPL مع المستخدم، دعنا نقول فقط أن البرنامج ينتهي به الأمر بكتلة من الشيفرة التي يريد تشغيلها.
  5. إذا كان كتلة الكود التي سيتم تشغيلها في ملف، jl_load(char *filename) يتم استدعاؤه لتحميل الملف و parse له. يتم تمرير كل جزء من الكود بعد ذلك إلى eval لتنفيذه.
  6. كل جزء من الشيفرة (أو AST) يتم تسليمه إلى eval() لتحويله إلى نتائج.
  7. eval() تأخذ كل جزء من الشيفرة وتحاول تشغيله في jl_toplevel_eval_flex().
  8. jl_toplevel_eval_flex() يحدد ما إذا كان الكود هو إجراء "على المستوى الأعلى" (مثل using أو module)، وهو ما سيكون غير صالح داخل دالة. إذا كان الأمر كذلك، فإنه يمرر الكود إلى المترجم على المستوى الأعلى.
  9. jl_toplevel_eval_flex() ثم expands الكود لإزالة أي ماكرو ولجعل AST "أبسط" للتنفيذ.
  10. jl_toplevel_eval_flex() ثم يستخدم بعض القواعد البسيطة لتحديد ما إذا كان يجب تجميع AST باستخدام JIT أو تفسيره مباشرة.
  11. تتم معالجة الجزء الأكبر من العمل لتفسير الشيفرة بواسطة eval in interpreter.c.
  12. إذا تم تجميع الشيفرة بدلاً من ذلك، فإن الجزء الأكبر من العمل يتم التعامل معه بواسطة codegen.cpp. كلما تم استدعاء دالة جوليا للمرة الأولى مع مجموعة معينة من أنواع الوسائط، سيتم تشغيل type inference على تلك الدالة. تُستخدم هذه المعلومات من قبل خطوة codegen لتوليد شيفرة أسرع.
  13. في النهاية، يقوم المستخدم بإنهاء REPL، أو يتم الوصول إلى نهاية البرنامج، وتقوم طريقة _start() بإرجاع القيمة.
  14. قبل الخروج مباشرة، تستدعي main() jl_atexit_hook(exit_code). هذا يستدعي Base._atexit() (الذي يستدعي أي دوال مسجلة لـ atexit() داخل جوليا). ثم يستدعي jl_gc_run_all_finalizers(). أخيرًا، يقوم بتنظيف جميع مقبضات libuv بشكل سلس وينتظر حتى يتم تفريغها وإغلاقها.

Parsing

المحلل اللغوي لجوليا هو برنامج لسب صغير مكتوب بلغة فيمتوليسب، يتم توزيع شفرة المصدر الخاصة به داخل جوليا في src/flisp.

تُعرّف وظائف الواجهة لهذا بشكل أساسي في jlfrontend.scm. الكود في ast.c يتعامل مع هذا الانتقال من جانب جوليا.

الملفات الأخرى ذات الصلة في هذه المرحلة هي julia-parser.scm، التي تتعامل مع تقسيم كود جوليا وتحويله إلى شجرة تحليل مجردة (AST)، و julia-syntax.scm، التي تتعامل مع تحويل تمثيلات AST المعقدة إلى تمثيلات AST أبسط، "مخفضة"، والتي تكون أكثر ملاءمة للتحليل والتنفيذ.

إذا كنت ترغب في اختبار المحلل دون إعادة بناء جوليا بالكامل، يمكنك تشغيل الواجهة الأمامية بمفردها على النحو التالي:

$ cd src
$ flisp/flisp
> (load "jlfrontend.scm")
> (jl-parse-file "<filename>")

Macro Expansion

عندما يواجه eval() ماكرو، فإنه يقوم بتوسيع تلك العقدة في شجرة التركيب (AST) قبل محاولة تقييم التعبير. يتضمن توسيع الماكرو نقلًا من 4d61726b646f776e2e436f64652822222c20226576616c28292229_40726566 (في جوليا) إلى دالة التحليل jl_macroexpand() (المكتوبة في flisp) إلى ماكرو جوليا نفسه (المكتوب في - ماذا أيضًا - جوليا) عبر fl_invoke_julia_macro()، والعودة.

عادةً ما يتم استدعاء توسيع الماكرو كخطوة أولى أثناء استدعاء Meta.lower()/jl_expand()، على الرغم من أنه يمكن أيضًا استدعاؤه مباشرةً من خلال استدعاء macroexpand()/jl_macroexpand().

Type Inference

يتم تنفيذ استنتاج النوع في جوليا بواسطة typeinf() in compiler/typeinfer.jl. استنتاج النوع هو عملية فحص دالة جوليا وتحديد الحدود لأنواع كل من متغيراتها، بالإضافة إلى الحدود على نوع قيمة الإرجاع من الدالة. وهذا يمكّن العديد من التحسينات المستقبلية، مثل إزالة الصناديق للقيم الثابتة المعروفة، ورفع العمليات المختلفة في وقت التشغيل مثل حساب إزاحات الحقول ومؤشرات الدوال في وقت الترجمة. قد يتضمن استنتاج النوع أيضًا خطوات أخرى مثل نشر الثوابت والتضمين.

More Definitions
  • JIT

    التجميع عند الطلب هي عملية توليد كود الآلة الأصلي في الذاكرة في الوقت الذي يحتاج فيه.

  • LLVM

    آلة افتراضية منخفضة المستوى (مترجم) مترجم JIT لجوليا هو برنامج/مكتبة تُسمى libLLVM. يشير التوليد البرمجي في جوليا إلى كل من عملية أخذ شجرة التركيب المجردة لجوليا وتحويلها إلى تعليمات LLVM، وعملية تحسين LLVM لذلك وتحويله إلى تعليمات تجميع أصلية.

  • C++

    لغة البرمجة التي تم تنفيذ LLVM بها، مما يعني أن توليد الشيفرة (codegen) تم أيضًا تنفيذه بهذه اللغة. بقية مكتبة جوليا (Julia) تم تنفيذها بلغة C، جزئيًا لأن مجموعة ميزاتها الأصغر تجعلها أكثر قابلية للاستخدام كطبقة واجهة عبر اللغات.

  • صندوق

    يستخدم هذا المصطلح لوصف عملية أخذ قيمة وتخصيص غلاف حول البيانات التي تتعقبها جامع القمامة (gc) والتي يتم وضع علامة عليها بنوع الكائن.

  • unbox

    عكس تغليف قيمة. تتيح هذه العملية معالجة أكثر كفاءة للبيانات عندما يكون نوع تلك البيانات معروفًا تمامًا في وقت الترجمة (من خلال استنتاج النوع).

  • دالة عامة

    دالة جوليا تتكون من عدة "طرق" يتم اختيارها للت dispatch الديناميكي بناءً على نوع توقيع الوسائط

  • دالة مجهولة أو "طريقة"

    دالة جوليا بدون اسم وبدون قدرات توجيه نوع البيانات

  • دالة بدائية

    دالة تم تنفيذها بلغة C ولكن تم عرضها في Julia كدالة مسماة "method" (على الرغم من عدم وجود قدرات توصيل دالة عامة، مشابهة لدالة مجهولة)

  • دالة داخلية

    عملية منخفضة المستوى مكشوفة كدالة في جوليا. هذه الدوال الزائفة تنفذ عمليات على البتات الخام مثل الجمع وتمديد الإشارة التي لا يمكن التعبير عنها مباشرة بأي طريقة أخرى. نظرًا لأنها تعمل على البتات مباشرة، يجب تجميعها في دالة ومحاطتها باستدعاء لـ Core.Intrinsics.box(T, ...) لإعادة تعيين معلومات النوع إلى القيمة.

JIT Code Generation

Codegen هو عملية تحويل شجرة التركيب المجردة (AST) لجوليا إلى كود آلة أصلي.

يتم تهيئة بيئة JIT من خلال استدعاء مبكر إلى jl_init_codegen in codegen.cpp.

عند الطلب، يتم تحويل طريقة جوليا إلى دالة أصلية بواسطة الدالة emit_function(jl_method_instance_t*). (ملاحظة، عند استخدام MCJIT (في LLVM v3.4+)، يجب تجميع كل دالة في وحدة جديدة.) تستدعي هذه الدالة بشكل متكرر emit_expr() حتى يتم إصدار الدالة بالكامل.

يكرس الجزء المتبقي من هذا الملف للعديد من التحسينات اليدوية لأنماط الشيفرة المحددة. على سبيل المثال، emit_known_call() يعرف كيفية تضمين العديد من الدوال الأولية (المعرفة في builtins.c) لمجموعات مختلفة من أنواع الوسائط.

تتم معالجة أجزاء أخرى من codegen بواسطة ملفات مساعدة متنوعة:

  • debuginfo.cpp

    يتعامل مع تتبعات الأخطاء لوظائف JIT

  • ccall.cpp

    يتعامل مع FFI الخاص بـ ccall و llvmcall، جنبًا إلى جنب مع ملفات abi_*.cpp المختلفة

  • intrinsics.cpp

    يتعامل مع انبعاث وظائف داخلية منخفضة المستوى متنوعة

Bootstrapping

عملية إنشاء صورة نظام جديدة تُسمى "التمهيد".

أصل هذه الكلمة يأتي من العبارة "سحب النفس من خلال حبال الأحذية"، ويشير إلى فكرة البدء من مجموعة محدودة جدًا من الوظائف والتعريفات المتاحة وانتهاءً بإنشاء بيئة كاملة الميزات.

System Image

صورة النظام هي أرشيف مُسبق التجميع لمجموعة من ملفات جوليا. ملف sys.ji الموزع مع جوليا هو واحدة من هذه الصور النظامية، تم إنشاؤها عن طريق تنفيذ الملف sysimg.jl، وتسلسل البيئة الناتجة (بما في ذلك الأنواع، الوظائف، الوحدات، وجميع القيم المعرفة الأخرى) إلى ملف. لذلك، تحتوي على نسخة مجمدة من وحدات Main و Core و Base (وأي شيء آخر كان في البيئة في نهاية عملية التمهيد). يتم تنفيذ هذا التسلسل/إلغاء التسلسل بواسطة jl_save_system_image/jl_restore_system_image in staticdata.c.

إذا لم يكن هناك ملف sysimg (jl_options.image_file == NULL)، فهذا يعني أيضًا أنه تم إعطاء --build في سطر الأوامر، لذا يجب أن تكون النتيجة النهائية ملف sysimg جديد. أثناء تهيئة جوليا، يتم إنشاء وحدات Core و Main الأساسية. ثم يتم تقييم ملف يسمى boot.jl من الدليل الحالي. بعد ذلك، تقوم جوليا بتقييم أي ملف تم إعطاؤه كوسيط سطر الأوامر حتى تصل إلى النهاية. أخيرًا، يتم حفظ البيئة الناتجة في ملف "sysimg" لاستخدامه كنقطة انطلاق لجولة جوليا مستقبلية.