Eval of Julia code
أحد أصعب الأجزاء في تعلم كيفية تشغيل لغة جوليا للكود هو تعلم كيفية عمل جميع الأجزاء معًا لتنفيذ كتلة من الكود.
كل جزء من الكود عادة ما يمر بعدة خطوات بأسماء قد تكون غير مألوفة، مثل (دون ترتيب معين): flisp، AST، C++، LLVM، eval
، typeinf
، macroexpand
، sysimg (أو صورة النظام)، التمهيد، التجميع، التحليل، التنفيذ، JIT، التفسير، التعبئة، التفريغ، الدالة الداخلية، والدالة الأولية، قبل أن يتحول إلى النتيجة المرغوبة (نأمل).
Julia Execution
وجهة نظر 10,000 قدم من العملية الكاملة هي كما يلي:
- يبدأ المستخدم
julia
. - تتم استدعاء دالة C
main()
منcli/loader_exe.c
. تقوم هذه الدالة بمعالجة وسائط سطر الأوامر، مما يملأ هيكلjl_options
ويضبط المتغيرARGS
. ثم تقوم بتهيئة جوليا (عن طريق استدعاءjulia_init
ininit.c
، والذي قد يقوم بتحميل sysimg الذي تم تجميعه مسبقًا. أخيرًا، تقوم بنقل التحكم إلى جوليا عن طريق استدعاءBase._start()
. - عندما تتولى
_start()
السيطرة، تعتمد سلسلة الأوامر اللاحقة على وسائط سطر الأوامر المقدمة. على سبيل المثال، إذا تم تزويد اسم ملف، فسوف تتابع لتنفيذ ذلك الملف. خلاف ذلك، ستبدأ في واجهة تفاعلية REPL. - تخطي التفاصيل حول كيفية تفاعل REPL مع المستخدم، دعنا نقول فقط أن البرنامج ينتهي به الأمر بكتلة من الشيفرة التي يريد تشغيلها.
- إذا كان كتلة الكود التي سيتم تشغيلها في ملف،
jl_load(char *filename)
يتم استدعاؤه لتحميل الملف و parse له. يتم تمرير كل جزء من الكود بعد ذلك إلىeval
لتنفيذه. - كل جزء من الشيفرة (أو AST) يتم تسليمه إلى
eval()
لتحويله إلى نتائج. eval()
تأخذ كل جزء من الشيفرة وتحاول تشغيله فيjl_toplevel_eval_flex()
.jl_toplevel_eval_flex()
يحدد ما إذا كان الكود هو إجراء "على المستوى الأعلى" (مثلusing
أوmodule
)، وهو ما سيكون غير صالح داخل دالة. إذا كان الأمر كذلك، فإنه يمرر الكود إلى المترجم على المستوى الأعلى.jl_toplevel_eval_flex()
ثم expands الكود لإزالة أي ماكرو ولجعل AST "أبسط" للتنفيذ.jl_toplevel_eval_flex()
ثم يستخدم بعض القواعد البسيطة لتحديد ما إذا كان يجب تجميع AST باستخدام JIT أو تفسيره مباشرة.- تتم معالجة الجزء الأكبر من العمل لتفسير الشيفرة بواسطة
eval
ininterpreter.c
. - إذا تم تجميع الشيفرة بدلاً من ذلك، فإن الجزء الأكبر من العمل يتم التعامل معه بواسطة
codegen.cpp
. كلما تم استدعاء دالة جوليا للمرة الأولى مع مجموعة معينة من أنواع الوسائط، سيتم تشغيل type inference على تلك الدالة. تُستخدم هذه المعلومات من قبل خطوة codegen لتوليد شيفرة أسرع. - في النهاية، يقوم المستخدم بإنهاء REPL، أو يتم الوصول إلى نهاية البرنامج، وتقوم طريقة
_start()
بإرجاع القيمة. - قبل الخروج مباشرة، تستدعي
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
. استنتاج النوع هو عملية فحص دالة جوليا وتحديد الحدود لأنواع كل من متغيراتها، بالإضافة إلى الحدود على نوع قيمة الإرجاع من الدالة. وهذا يمكّن العديد من التحسينات المستقبلية، مثل إزالة الصناديق للقيم الثابتة المعروفة، ورفع العمليات المختلفة في وقت التشغيل مثل حساب إزاحات الحقول ومؤشرات الدوال في وقت الترجمة. قد يتضمن استنتاج النوع أيضًا خطوات أخرى مثل نشر الثوابت والتضمين.
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 بواسطة ملفات مساعدة متنوعة:
يتعامل مع تتبعات الأخطاء لوظائف JIT
يتعامل مع FFI الخاص بـ ccall و llvmcall، جنبًا إلى جنب مع ملفات
abi_*.cpp
المختلفةيتعامل مع انبعاث وظائف داخلية منخفضة المستوى متنوعة
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" لاستخدامه كنقطة انطلاق لجولة جوليا مستقبلية.