
# آلة البايت كود

مسار التنفيذ الافتراضي هو متجوّل شجرة: يزور كل عقدة AST ويعيد قيمة. أما الدوال كثيرة الاستخدام التي تُستدعى ملايين المرات، فتكون كلفة الإرسال لكل عقدة هي عنق الزجاجة. يضيف Phasis فوق ذلك مترجمًا تخمينيًا يحوّل البايت كود إلى إغلاقات PHP لتقليل هذه الكلفة.

## متى تبدأ الآلة الافتراضية العمل

لكل `JsFunction` عدّاد للمكالمات الساخنة. عندما يتجاوز حدًا معينًا (100 افتراضيًا)، يحاول المحرك ترجمة جسم الدالة إلى إغلاق PHP عبر `src/Bytecode/JsToPhp.php`.

ينجح التجميع عندما يطابق جسم الدالة مجموعة معروفة من البنى:

- إسناد المتغيرات المحلية بأنواع متوقعة (رقم، كائن، دالة، مصفوفة، سلسلة).
- العمليات الحسابية على الأعداد.
- قراءات الأعضاء من المتغيرات المحلية ذات نوع الكائن وكذلك من `this` (`this.v` و`obj.x`).
- استدعاءات الطرائق على المتغيرات المحلية ذات نوع الكائن (`obj.method(args)`)، مع تمرير المستقبل عبر `$thisValue`.
- استدعاءات المُنشئ `new Ctor(args)` — ترفع نوع المتغير المحلي في الطرف الأيسر إلى `object` وتحمي بتراجع حالة إرجاع قيمة غير كائنية.
- حلقات `for` / `while` ذات شروط بسيطة البنية.
- `return` بتعبير قابل للحساب.
- `if` / `else` على شروط ذات أنواع محددة.

يفشل التجميع (وتظل الدالة ضمن متجوّل الشجرة) إذا احتوى الجسم على:

- عبارات `with`.
- `try / catch / finally` (يعالجها متجوّل الشجرة لضمان دلالات الإكمال الصحيحة).
- استدعاءات `eval()`.
- تعديل مباشر لـ`arguments`.
- أي شيء لم يعرف المترجم بعد كيفية تحويله.

عند نجاح التجميع، يبدّل المحرك مسار استدعاء الدالة إلى الإغلاق المجمّع. وتُنفّذ الاستدعاءات اللاحقة شيفرة PHP أصلية بكلفة استدعاء دالة واحدة، لا كلفة لكل عقدة AST.

## التراجع

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

هنا تبرز أهمية مطابقة المواصفات. يجب أن يتمكن متجوّل الشجرة من إعادة تنفيذ الاستدعاء الذي تراجع من البداية دون ملاحظة أي آثار جانبية من المسار السريع الفاشل. يتعامل Phasis مع هذا بطريقتين:

1. **بادئات خالصة** — يفحص المترجم الآثار الجانبية قبل أي تخمين. إذا نفّذت الدالة إدخالًا/إخراجًا (console.log أو دوال المضيف) قبل نقطة التخمين، فلا يحذفها المسار السريع؛ بل يثبتها ويتابع.
2. **رفض وقت التجميع** — يرفض المترجم التجميع كليًا للبنى التي تتطلب التراجع عن آثار جانبية يمكن ملاحظتها (إسناد `let r = fn()` غير رقمي عندما يغيّر `fn()` الحالة). وتظل الدالة ضمن متجوّل الشجرة.

الحالة الدافعة: ينفّذ إسناد غير رقمي `fn()` في المسار السريع، ويطلق `Bailout`، ثم يُعاد تنفيذه في متجوّل الشجرة، ما يؤدي إلى تشغيل `fn()` مرتين مع مجموعتين من الآثار الجانبية. يمنع الرفض وقت التجميع تشغيل `fn()` في المسار السريع أصلًا، فلا يرى متجوّل الشجرة إلا الاستدعاء الوحيد.

## إرسال الآلة الافتراضية (الطبقة الثانية)

تُترجم بعض الدوال إلى إغلاقات PHP، بينما تنتقل أخرى إلى مفسّر بايت كود أبسط في `src/Bytecode/VM.php`. والبايت كود مصفوفة مسطّحة من رموز العمليات (`LOAD_LOCAL` و`STORE_PROP` و`CALL_METHOD`، …) يستهلكها إرسال عبر `switch`.

توجد الآلة الافتراضية للدوال التي لا يستحق الأمر ترجمتها إلى إغلاق PHP، لكنها تظل كثيرة الاستخدام بما يكفي للاستفادة من تجاوز اجتياز AST. تمر استدعاءات الطرائق على الكائنات المضمّنة (Array.prototype.map وString.prototype.split، …) عبر مسارات سريعة في الآلة الافتراضية تتجاوز آلية `callFunction` العامة.

## مكدس استدعاءات مخصص

تستخدم `Op::CALL` و`Op::CALL_METHOD` و`Op::NEW_CALL` مسار استدعاء مضمنًا يبقي مكدس استدعاءات JS بالكامل داخل `VM::execute`، دون إطار PHP جديد لكل استدعاء JS. عند الدخول، تُدفع حالة المستدعي (pc، وتدفّق الشيفرة، ومكدس المعاملات، والمتغيرات المحلية، والبيئة، و`this`، وعلامة الوضع الصارم) إلى مجموعة `savedFrames` لكل آلة افتراضية تضم نسخ `SavedFrame`، ثم تعيد حلقة الإرسال ربط متغيراتها المحلية بالدالة المستدعاة. وبعدها تستعيد `Op::RET` الإطار المحفوظ. وتفكّ الاستثناءات الإطارات المحفوظة قبل الرجوع إلى متجوّل الشجرة.

تُحفظ أهلية التنفيذ مؤقتًا على `JsFunction`: تكون `vmInlineCallCache` للاستدعاءات العادية (جسم مترجم بـBC، غير مرتبط بـ`bind()`، وغير أصلي)، و`vmInlineConstructCache` لتعبيرات `new` (بالإضافة إلى `isConstructable`، وليست مشتقة، ولا تحتوي على مُهيّئات لحقول النسخة أو للطرائق الخاصة). وما عدا ذلك يمر عبر آلية `callFunction` / `vmNewExpression` القياسية.

ومن الفوائد المعمارية الجانبية أن سقف مكدس PHP لم يعد يحد عمق الاستدعاء التكراري في JS. الحد الوحيد الآن هو `CallStack::maxDepth`، المضبوط على **4 096**؛ وهو أعلى بكثير من كل حالات الاختبار المشروعة في test262 (يحتاج معظمها إلى أقل من 2 k إطار) وضيق بما يكفي لإفشال اختبارات التكرار اللانهائي المرضية سريعًا ضمن مهلة المشغّل. ومسار الإغلاق المحوّل (`JsToPhp`) له حارسه الخاص البالغ 8 192 إطارًا لخيار التراجع غير المضمّن.

## مولّدات لقطة الإطار

تُترجم أجسام المولّدات البسيطة إلى شيفرة بايتية — بلا `yield*`، ولا `yield` داخل try/catch/finally، ولا `await`. يُصدر المصرّف تعليمة `Op::YIELD` جديدة عند تعليق الجسم؛ وتلتقط `Op::YIELD` حالة الإرسال (cf, pc+1, stack, sp, locals, env, thisValue, strict) في كائن كومة `GeneratorSnapshot`، وتُعيد قيمة حارسة `YieldResult` من `VM::execute`. يخزّن المشغّل `JsGenerator` اللقطة ويمرّرها إلى `VM::execute` عند الاستدعاء التالي لـ `next()` / `throw()`، مستبدلًا قيمة الاستئناف بأعلى مكدس المعاملات.

يحلّ هذا محل مسار PHP-Fiber للجزء المدعوم. يظل المشغّل المعتمد على Fiber (`executeGeneratorBody`) قائمًا للأشكال التي يرفضها المصرّف — التفويض بـ `yield*`، والمولّدات التي تنفّذ yield داخل كتلة try، والمولّدات غير المتزامنة، والدوال غير المتزامنة التي تتضمن `await`. تعمل `JsGenerator` في وضعين: يتحقق كل من `next()` / `throw()` / `return()` مما إذا كان المثيل يملك لقطة أو Fiber، ثم يوجّه التنفيذ وفقًا لذلك.

لا توجد معلمة قابلة للضبط عبر أداة القياس؛ إذ يُحسم الأهلية وقت الترجمة ويُحفظ القرار مؤقتًا على `JsFunction`. يوفّر مسار اللقطة تبديل سياق PHP أقل لكل yield؛ أما مسار Fiber فيبقى كما هو لكل ما عدا ذلك.

## ذاكرات التخزين المؤقت المضمّنة

يحمل `Op::LOAD_MEMBER` ذاكرة تخزين مؤقت مضمّنة لكل موضع تعليمة، ومفتاحها هوية النموذج الأولي؛ وتُستخدم لنمط أسلوب النموذج الأولي على المثيل (`c.sum(...)`، `arr.push(...)`، وكل استدعاء لأسلوب صنف). يتحقق التطابق عندما لا يكون للمستقبِل خانة خاصة بالاسم، ويكون مرجع النموذج الأولي المخزّن مطابقًا لمرجع `getPrototype()` لدى المستقبِل، وتظل `PropertyMap::$version` الخاصة بالنموذج الأولي مطابقة للإصدار المسجّل.

يعتمد إبطال ذاكرة التخزين المؤقت على `PropertyMap::$version`. تؤدي التغييرات البنيوية (التعيين / الحذف) إلى زيادته دائمًا. أما استبدال قيم خانات البيانات فيزيده فقط عندما تكون `PropertyMap::$isUsedAsProto` مفعّلة — وهي علامة حاجز كتابة تُضبط أول مرة يُلاحظ فيها أن الكائن هو `[[Prototype]]` لكائن آخر. لا تتحمل كائنات المثيلات التي لا تُستخدم أبدًا كنماذج أولية أي كلفة لبنية IC مع كل `obj.x = v`.

لا تُخزّن أدوات الوصول getter مؤقتًا أبدًا (إذ يجب إعادة تشغيلها عند كل قراءة).

يحمل `PropertyMap` عدّادًا ثانيًا هو `$mutationVersion`، يزداد *دائمًا* مع كل كتابة (من دون حاجز كتابة). وتثبّت ذاكرة التخزين المؤقت `JSON.stringify` هذا العدّاد لكل كائن أو مصفوفة متداخلة تتم زيارتها، لكي يؤدي تغيير يخص المثيل وحده مثل `o.a = 99` إلى إبطال السلسلة المخزّنة مؤقتًا بشكل صحيح، حتى عندما كانت `$version` لدى IC ستتجاوز الزيادة.

## ذاكرة JSON.stringify المؤقتة

يحمل `JsObject` كلًا من `$jsonCacheString` + `$jsonCacheVersions` (قائمة أزواج `[obj, mutationVersion]` لكل كائن متداخل تمت زيارته أثناء المرور المبسّط لتحويله إلى سلسلة). في الاستدعاء التالي لـ `JSON.stringify(o)` من دون بديل أو مسافات، يتحقق المسار السريع المبسّط من أن كل إصدار مثبّت لم يتغير، ثم يعيد السلسلة المخزّنة من دون إعادة المرور أو الترميز. تحقق أنماط النسخ العميق المتكررة مثل `JSON.parse(JSON.stringify(o))` تسريعًا يقارب الضعف في مرحلة تحويل الكائن إلى سلسلة.

تُسقط الذاكرة المؤقتة بصمت عند عدم التطابق (أي اختلاف أي إصدار مثبّت)، وتُملأ من جديد في المرور التالي. تنتقل الكائنات التي تتضمن `toJSON` أو أدوات getter أو أدوات وصول أو رموزًا أو خانات `[[PrimitiveValue]]` إلى أداة التسلسل المطابقة للمواصفات، ولا تملأ الذاكرة المؤقتة أبدًا؛ لذا لا يتعامل المسار المخزّن مؤقتًا إلا مع بيانات متجانسة بنيويًا.

## المسارات السريعة المضمّنة

تحققت أكبر مكاسب الأداء عندما علّمنا VM التعامل مع مكوّنات مضمّنة محددة:

- **`Atomics.load` / `store` / `compareExchange`** مع الجدولة التعاونية `loadSpinHook` / `storeNotifyHook` — استعاد ذلك 4 اختبارات لحلقات الدوران في Atomics.
- **`Date.prototype.getTimezoneOffset`** مع ذاكرة DST مؤقتة مباشرة عبر hashmap — تسريع بنسبة 45 % في اختبار ضغط DST لـ SpiderMonkey.
- **`String.fromCharCode`** مع مسار سريع مضمّن لـ ASCII — يوفر نحو 1 M عملية إرسال لكل دورة نموذجية.
- **`decodeURI` / `decodeURIComponent`** مع مسار فائق السرعة لـ `%XX%XX%XX%XX` من 12 محرفًا يستخدم فك ترميز جماعيًا `hex2bin`.
- **`Object.keys` / `Object.values` / `Object.entries`** مع الوصول المباشر إلى خريطة الخصائص.
- **`Array.prototype.map` / `filter` / `forEach` / `reduce`** مع التكرار الأصلي فوق مصفوفة PHP الأساسية.

يُفعّل كل مسار سريع بعد «فحص الشكل» — فإذا لم يطابق المستقبِل الشكل المتوقع (e.g، مثل عرض TypedArray غير مدعوم بـ PHP `string` عادي)، ينتقل VM إلى المسار البطيء المطابق للمواصفات.

## ما الذي لا يفعله هذا

إن VM للشيفرة البايتية **ليس JIT** بالمعنى المستخدم في V8. فهو لا ينشئ شيفرة آلة أصلية لكل دالة، ولا يتكهّن استنادًا إلى الأنواع المرصودة، باستثناء فحص هوية النموذج الأولي أحادي الشكل في IC. ويُحاكى تضمين حدود الدوال باستخدام مكدس الاستدعاءات المخصص (من دون إطار PHP إضافي لكل استدعاء JS)، لا بخفض جسم الدالة المستدعاة إلى شيفرة بايتية للدالة المستدعية.

يُعدّ محوّل JsToPhp أقرب ما لدى المحرك إلى JIT: فهو ينشئ مصدر PHP لأجسام الدوال الساخنة، ثم يترجمه JIT التتبّعي في الإصدار 8.5 من PHP إلى شيفرة أصلية. يمر JsToPhp عبر `eval()`، لذا يتحمل كلفة ترجمة لمرة واحدة، وعلى JIT التتبّعي اكتشاف الدالة كما لو كانت شيفرة مستخدم. إن تخطي خطوة `eval()` بإصدار تعليمات opcache مباشرة سيقلّص الفجوة أكثر — راجع [المعايير § خارطة الطريق](https://phasis.inline0.dev/ar/docs/advanced/benchmarks#roadmap).

## قياس الأداء

يشغّل `bench/microbench.js` نحو اثني عشر معيارًا صغيرًا (loop-arith, loop-fib, fn-recurse, fn-deep-recurse, obj-create, obj-prop, proto-method, gen-iterate, arr-push, arr-map, str-concat, str-split-join, json-roundtrip, closure, destructure). الزمن الوسيط الفعلي:

```bash
php bench/run.php
```

تُحفظ الأرقام الحالية في `BENCH.md` بعد كل تشغيل لسير عمل `bench`. ويُشغّل سير عمل `bench` ومصفوفة test262 يدويًا.
