صفحات التوثيق
الملكية والإتلاف
تعطي السلطان كل قيمة مالكة مالكًا واحدًا، وتحررها تلقائيًا حين يخرج مالكها من نطاقه. تحدد هذه الصفحة قاعدة الملكية
التي يفرضها المترجم، والنسخ الصريح بـ استنسخ، والإتلاف التلقائي ومخصص الذاكرة الذي يعتمد عليه، وبدائيات
المتجهات المبنية عليه، وبيئات الدوال المغلقة، والقيود الباقية. وهي موجهة إلى المبرمجين الذين يحتاجون القواعد الدقيقة،
وإلى المساهمين الذين يعملون على تحليل الأمان وخفض MSIR.
الأنواع المالكة والأنواع البسيطة
يعلّم تصنيف الذاكرة (compiler/frontend/memory) النوع بأنه مالك (يتطلب_تحريرا) إذا كان يحمل نصًا أو متجهًا أو
صندوقًا أو عضوًا مركبًا يحمل أحدها. وكل نوع آخر بسيط، وقراءة القيمة البسيطة تنسخها. الأعداد والحروف والقيم المنطقية
بسيطة، وكذلك السجلات المكونة من أعضاء بسيطة فقط. أما نص ومتجه وأي سجل يحتوي أحدهما فمالكة.
قاعدة الملكية
يفرض تحليل الأمان، SSF (compiler/frontend/ssf)، الملكية الفردية للقيم المالكة، متخذًا E0507 في Rust نموذجًا.
استعمال القيمة المالكة ينقلها، ولا يجوز استعمال الاسم القديم بعد ذلك. والدالة التي تأخذ معاملها بالقيمة تستهلك
الوسيط؛ أما الدالة التي تأخذ &T أو &متغير T فتستعيره فقط.
استورد المكتبة.دخ.println;
استورد المكتبة.نصوص.number;
نوع حساب = { المالك: نص, الرصيد: عدد };
عرف أودع(الحساب: &متغير حساب, المقدار: عدد): فراغ {
(*الحساب).الرصيد := (*الحساب).الرصيد + المقدار;
}
عرف أغلق(الحساب: حساب): نص {
أرجع استنسخ(&الحساب.المالك);
}
عرف الرئيسية(): عدد {
دع الحساب: متغير حساب = { المالك: "ليلى", الرصيد: 100 };
أودع(&متغير الحساب, 50);
اطبع_سطر(عدد_إلى_نص(الحساب.الرصيد));
اطبع_سطر(أغلق(الحساب));
أرجع 0;
}
بعد أغلق(الحساب) تكون القيمة قد نُقلت، وأي استعمال لاحق لـ الحساب هو SULTANC-OWNERSHIP-USE-AFTER-MOVE.
ويشير التشخيص إلى المالك الأصلي وإلى موضع النقل.
للقاعدة هذه الأجزاء:
- لا نقل من تخزين مستعار (
SULTANC-OWNERSHIP-MOVE-OUT-OF-REFERENCE). يُرفض كل استعمال كان سينقل قيمة مالكة من تخزين مستعار. ويشمل ذلك إلغاء الإشارة*ر، والإسقاط عبر مرجع مثل(*ر).حأو(*ر)[ي]، والعرض المستعار. - العروض المستعارة ليست نقلًا. التصريح غير القابل للتعديل
دع س: T = *ر;(أو الإسقاط عبرر)، وربط النمط الذي يكون المطابَق فيه تخزينًا مستعارًا، يقرآن التخزين المستعار في موضعه. والعرض لا يُتلف أبدًا ولا يمكن نقله. - ينتهي العرض حين يُكتب التخزين الذي يقرؤه. والكتابات هي: الإسناد عبر مرجعه؛ و
ألحقأوبدّلأوأزل_الأخيرأواقتطععبره؛ وتمرير المرجع، أو متغير محلي يحمل مرجعًا حصريًا، وسيطًا من نوع&متغير. واستعمال العرض بعد انتهائه يُبلَّغ عنه استعمالًا بعد النقل (SULTANC-OWNERSHIP-USE-AFTER-MOVE). وينهي Rust الاستعارة عند الكتابة المتعارضة (NLL) بالطريقة نفسها. - الصناديق. نقل قيمة مالكة من صندوق مملوك يستهلك الصندوق. وقراءة قيمة بسيطة عبر صندوق أو مرجع لا تنقل أبدًا.
- الاستعارات تتعارض. لا يجوز أن تجتمع استعارة كتابة مع استعارة أخرى للقيمة نفسها (
SULTANC-BORROW-CONFLICT).
القوالب العامة: قيود البساطة المستنتجة
يُفحص القالب العام (generic) قبل التعويض، ويُعامل كل معامل نوع فيه على أنه مالك. وبعض القوالب تنسخ قيمة من نوع
معامل النوع P نفسه من تخزين مستعار، مثل البحث في خريطة الذي يعيد القيمة المخزنة. مثل هذا القالب يحصل على قيد
بساطة مستنتج على P:
- يعيد SSF تحليل الدالة مع اعتبار
Pبسيطًا (قيد_معامل_نوع_عاديا،compiler/frontend/memory/semantic_pipeline.sn). - ينتشر القيد عبر الاستدعاءات العامة المسجلة في حقائق الاستدعاء. فحين يُنشأ
F<P>بمعاملQمن المستدعي، يحصلQعلى القيد أيضًا (انشر_قيود_عادية_لتدفق). - كل استدعاء ينشئ معاملًا مقيدًا بنوع مالك يُرفض عند ذلك الاستدعاء (
تحقق_قيود_عادية_لتدفق).
ويعبّر Rust عن المتطلب نفسه صراحة بـ T: Copy. فـ ابحث_في_الخريطة<K, ص32> مقبول، وابحث_في_الخريطة<K, نص> مرفوض عند
الاستدعاء، لأن البحث كان سينسخ نصًا مملوكًا من الخريطة.
النسخ
استنسخ(&س) بدائية في المترجم (هوية_بدائي_مترجم.استنساخ). ونتيجتها نسخة عميقة مستقلة من النوع
المشار إليه (فئة_ناتج_بدائي.قيمة_المرجع). يصدر خفض التعابير هذه البدائية، ثم يعيد
اخفض_غراء_الملكية_مسير (compiler/msir/lowering/ownership_glue.sn) كتابتها إلى دوال نسخ مولدة لكل نوع (clone glue):
دوال MSIR عادية تُولَّد مرة واحدة لكل نوع محدد، كما يولد rustc دوالَّ وسيطة (shims). وتشغّل الخلفيات الأصلية وLLVM
والمفسر الدوال المولدة نفسها.
| النوع | دالة النسخ المولدة |
|---|---|
| متجه | متجه فارغ جديد محجوز بطول المصدر، ثم إلحاق نسخة من كل عنصر |
| نص | مثل متجه البايتات، ثم نقل الملكية إلى نص |
| سجل أو صف | نسخ كل عضو ثم إعادة التجميع |
| تعداد أو اختياري أو نتيجة | التوزيع بحسب الوسم ونسخ الحمولات في المنشئ نفسه |
| صندوق | صندوق جديد يحمل نسخة من الحمولة |
| نوع بسيط | التحميل وحده |
تُترجم شيفرة المترجم والمكتبة القياسية أولًا بـ Stage0، وهو المترجم الأولي (seed)، ولا يملك بدائية نسخ (انظر قيود شيفرة المترجم). ولذلك تصنع نسخًا مستقلة بدوال عادية لكل نوع، عميقة ومتطابقة في كل المراحل:
انسخ_بايتاتوانسخ_نصوانسخ_اختيار_نصوانسخ_اختيار_بايتات(library/std/text/from_bytes.sn) للبايتات والنصوص؛- دالة
انسخ_مملوكا_<النوع>واحدة لكل نوع مالك مسمى، موضوعة بجانب النوع، مثلانسخ_مملوكا_نوع_مقننفيcompiler/frontend/types/type_model.sn؛ انسخ_مملوكا_متجه_<النوع>لمتجهات نوع مسمى؛- بضع دوال مساعدة بنيوية خاصة (
انسخ_مملوكا__…) حيث لا يكون للنوع مالك مسمى.
وحيث ترفض قاعدة الملكية موضعًا ما، يتبع الإصلاح مقصد الشيفرة:
- الوصول للقراءة فقط يستعير. فاستعلامات السجل تطابق خانة السجل في موضعها، وتحويل JSON إلى نص يأخذ مراجع.
- النسخ ثم التعديل ثم إعادة الكتابة يصير تحديثًا في الموضع. وهكذا تعمل عمليات حالة SSF، ومجموعة التشخيصات، وإعادة الترقيم في MSIR، ومحلل JSON.
- أخذ الملكية من خانة يضع فيها قيمة فارغة بـ
بدّل. وهكذا يعمل اتصال عميل HTTP، وحالة تدفق TLS وأسراره، وحقول CSV، ونتائج PKCS#12، وعروض إنشاء النسخ المحددة من القوالب العامة (monomorphization)، ومُنهيات MSIR. - أنماط التفكيك تنقل الأجزاء المملوكة، كما في
| نتيجة.نجح({ البايتات: البايتات, … }). - القيمة المستقلة حقًا تُنسخ بدالتها الخاصة بنوعها. وهكذا تعمل لقطات الفروع لحالة التدفق والخفض، والقيم التي تعيدها واجهات الاستعلام العامة في المكتبة القياسية، والإنشاء العام.
الإتلاف
صُمم الإتلاف على نموذج مرحلة drop elaboration في MIR لدى Rust وOSSA لدى Swift. وهناك تنفيذ MSIR واحد محايد تجاه الهدف
(compiler/msir/lowering/drops.sn، ownership_glue.sn) تستهلكه بالطريقة نفسها الخلفيات الأصلية (x86_64 وARM64
وwasm32) وLLVM والمفسر. والثلاثة تستعمل بدائية تحرير نفسها ودوال MSIR نفسها من دوال الإتلاف المولدة لكل نوع (drop glue)؛ وليس لأي منها
تصور خاص لوقت إتلاف القيمة.
- حقائق الملكية للخفض (
compiler/frontend/ssf/ownership_facts.sn). يسجل SSF لكل دالة إزاحة البايت لكل استعمال لاسم محلي ينقل قيمة المتغير المحلي إلى خارجه، ويسجل أي المتغيرات المحلية عروض مستعارة. ولا يعيد الخفض أبدًا استنتاج دلالات النقل أو العرض؛ بل يستهلك هذه الحقائق فقط. وذراعطابقالتي تربط قيمة مالكة بالنقل من مطابَق مملوك ليس مرجعًا تنقل أيضًا المتغير المحلي الجذر للمطابَق، فلا يمكن قراءة المطابَق بعد ذلك. - إطار إتلاف لكل دالة (
drops.sn). يعطي الخفض كل دالة كتلة مقدمة تحجز، لكل متغير محلي مالك بترتيب التصريح، تخزينًا وعلم إتلاف منطقيًا. يبدأ العلم صحيحًا عند التصريح أو ربط المعامل أو ربط ذراع المطابقة، ويُمسح عند نقل مسجل أو تحرير صريح أو التقاط في دالة مغلقة. وكلأرجع، صريحًا كان أو بالوصول إلى نهاية الدالة، يمر عبر كتلة خروج مشتركة واحدة تتلف المالكين بعكس ترتيب التصريح، بحسب أعلامهم، قبل الرجوع الفعلي. وكذلك يتلفاخرج_من_حلقةأوتابع_حلقةفي الحلقة المتغيرات المحلية الخارجة من النطاق قبل القفز. والإتلاف هو بدائيةتحرير، وتُعاد كتابته إلى دالة الإتلاف المولدة للنوع. - دوال الإتلاف المولدة (
ownership_glue.sn، على مثال دوال النسخ المولدة). توجد دالة MSIR واحدة(T) -> ()لكل نوع مالك. تتلف المركبات أعضاءها المالكة. وتوزع القيم الموسومة بحسب الوسم وتتلف الحمولة. وتتلف المتجهات عناصرها ثم تحرر المخزن. وينقل النص مخزنه إلى متجه بايتات ويعيد استعمال جسم إتلاف المتجه. وتتلف الصناديق الحمولة ثم تحرر خلية الصندوق. وتمر كل استدعاءات التحرير عبر مزود التخصيص القانوني (مزودات_تخصيص_الذاكرة.التحرير)، الذي يكيّف وسيط المرجع (العنصر الأول في مخزن متجه أو نص) كما يكيّف حمولة الصندوق. - مخصص الذاكرة (
library/std/os/allocation.sn) يعيد استعمال ما يحرره الإتلاف. الكتل الصغيرة، حتى 4 KiB، يعاد استعمالها بحسب الحجم. والكتل الكبيرة تُقسم لتناسب الطلب وتُدمج مع جاراتها الكبيرة الحرة عند تحريرها، والكتلة المحررة التي تبلغ قمة الكومة تخفضها. وعلى الأهداف الأصلية تُعاد إلى نظام التشغيل الصفحاتُ الداخلية للكتل الكبيرة الحرة، والصفحات الملتزمة البعيدة فوق القمة. والكتلة التي في القمة تنمو في موضعها، والكتلة التي تليها كتلة كبيرة حرة تنمو داخلها. وتحرير عنوان داخل الكومة ليس كتلة حية (تحرير مزدوج، أو مؤشر داخلي) يوقف البرنامج. أما العنوان خارج الكومة، مثل التخزين الثابت الذي يحمل حرفيًا نصيًا مُرر قيمةً مملوكة، فيُتجاهل.
الحلقة التي تخصص سجلًا مالكًا وتتلفه 10^5 مرة، مع إهمال النتيجة في كل تكرار، تحافظ على مجموعة مقيمة ثابتة تقارب
300 KB أيًّا كان عدد التكرارات: فكتل الكومة تُحرر ويعاد استعمالها بدل أن تتراكم. ويصح هذا مع حرر صريح بجانب الإتلاف
التلقائي وبدونه، ولا ينتج عن ذلك تحرير مزدوج.
فهرس مواضع الرموز في SSF (compiler/frontend/ssf/local_state.sn، فهرس_مواضع_رموز_تدفق) متجه مسطح واحد مرتب يُنسخ في
تخصيص واحد، لأن وجود مالكين متشاركين في ظل الملكية الفردية كان سيعني تحريرًا مزدوجًا.
التفكيك
ذراع طابق الذي تنقل روابطه جزءًا مالكًا من مميز مملوك (متغير محلي مالك، أو قيمة مؤقتة مثل ناتج استدعاء) يستهلك
المميز. ويُتلف كل جزء منه لا يأخذه ربط مالك عند دخول الذراع (أسقط_أجزاء_غير_مربوطة_خفض، drops.sn):
- الجزء المطابق بـ
_، وحقل السجل الذي لا يسميه النمط؛ - الجزء المطابق بحرفي أو بمدى؛
- الجزء المربوط بروابط بسيطة فقط، أو بربط قيمة دالة مغلقة، وهو يحمل مرجعًا خاصًا به؛
- الأجزاء غير المربوطة من أنماط السجلات والصفوف والمنشئات المتداخلة، على أي عمق.
طابق الواجهة مع
| { الأنواع: الأنواع, الأسماء: _, الوحدات: _ } -> { أرجع الأنواع; };
هنا ينتقل الأنواع، ويُتلف الأسماء والوحدات عند دخول الذراع. ولا شيء في الذراع يمكنه تسمية المميز المستهلك،
فإتلاف الأجزاء هناك يكافئ إتلافها عند نهاية الذراع، ويشمل كل مخرج منه، ومنها أرجع واخرج_من_حلقة وتابع_حلقة.
والذراع الذي لا ينقل شيئًا يترك المميز لمالكه (يحتفظ به المتغير المحلي حتى نهاية نطاقه، وتُتلف القيمة المؤقتة عند
نهاية الذراع)، والمطابقة عبر مرجع لا تتلف شيئًا. يتلف Rust الحقول الباقية من قيمة منقولة جزئيًا عند نهاية نطاق
مالكها؛ والفرق في التوقيت وحده لا في حدوث الإتلاف. يتحقق tests/*/language/surface/destructuring_drops.sn بعدادات
المخصص من أن أي جزء لا يبقى خلفها.
استهلاك المترجم للذاكرة
يبقي الإتلاف مجموعة العمل الخاصة بالمترجم صغيرة. ذروة الذاكرة المقيمة عند ترجمة المترجم لنفسه من
compiler/main.sn على x86_64-linux (الخلفية الأصلية فقط، --emit=exe، مقيسة بـ /usr/bin/time -f %M):
| المترجم | بناه | ذروة الذاكرة المقيمة (RSS) |
|---|---|---|
| Stage1 | Stage0 المكتوب بلغة C (bootstrap/**) |
0.92 GB |
| Stage2 | Stage1 | 0.78 GB |
ويُدرج Stage0 أيضًا عمليات الإتلاف (bootstrap/src/llvm/llvm_aggregates.c، llvm_values.c، llvm_patterns.c): فهو
يتلف المتغيرات المحلية المملوكة عند الخروج من النطاقات، والقيم المحلية في الأذرع، والمطابَقات المؤقتة. ولذلك يحرر
Stage1 بيانات عمله، وتبقى ذروته في حدود 15% تقريبًا من ذروة Stage2.
بدائيات المتجهات
الإزالة من الآخر والاقتطاع
أزل_الأخير(م) واقتطع(م, ن) بدائيتان في المترجم على نموذج Vec::pop
وVec::truncate في Rust.
أزل_الأخيرتنقل العنصر الأخير إلى الخارج وتعيده بصيغةاختياري.موجود. والمتجه الفارغ يعطياختياري.لا_شيء. ويُنقل العنصر ولا يُنسخ: يتوقف المتجه عن ملكيته ويملكه المستدعي، فلا يُتلف شيء.اقتطعتحتفظ بأولنمن العناصر (ن: طبيعي، ويقابلهusizeفي Rust) وتتلف الباقي، من الأول إلى الأخير، عبر دوال الإتلاف المولدة لنوع عناصرها. وإذا كانتنمساوية للطول أو أكبر منه فلا يتغير شيء.
عرف الرئيسية(): عدد {
دع م: متغير متجه نص = ["أ", "ب", "ج"];
دع الأخير: اختياري نص = أزل_الأخير(م);
اقتطع(م, 1);
أرجع الطول(م) + سعة(م);
}
وكلتاهما، مثل ألحق، تكتب في وسيطها الأول، ويجب أن يكون موضعًا قابلًا للتعديل: متغيرًا محليًا، أو حقلًا، أو *ر
عبر &متغير، أو عنصرًا. ويرفض فاحص الأنواع أي شيء آخر بـ SULTANC-TYPE-MISMATCH. ويفحص SSF هذه الكتابة كما يفحص
الإلحاق: فهي تتعارض مع استعارة حية للمتجه (SULTANC-BORROW-CONFLICT) وتنهي العرض المقروء عبر المرجع نفسه، فيكون
الاستعمال اللاحق SULTANC-OWNERSHIP-USE-AFTER-MOVE. ونتيجة أزل_الأخير قيمة مملوكة جديدة.
الخفض (اخفض_إزالة_أو_اقتطاع_مسير، compiler/msir/lowering/expressions.sn) يصدر MSIR عاديًا على موضع المتجه.
تحمّل أزل_الأخير الترويسة، وتقرأ الطول، وتتفرع، وتقرأ العنصر الأخير باستخراج بالفهرس، وتبني الاختياري في كتلة التقاء. وتدور
اقتطع في حلقة من ن إلى الطول، مصدرةً بدائية التحرير على كل عنصر مالك، فيحولها تمرير الدوال المولدة للملكية إلى
دالة الإتلاف المولدة لنوع العنصر، فيُتلف كل من النصوص والمتجهات المتداخلة والسجلات والصناديق ومراجع الدوال المغلقة مرة واحدة بالضبط.
وتخزن بدائية داخلية واحدة، ضبط_طول_متجه، الطول الجديد: في x86_64 وarm64 تخزن سجلًا في كلمة الطول في الترويسة، وفي
wasm32 تعليمة i32.store، وفي LLVM تخزينًا بعرض الطول المساوي لحجم المؤشر (والأهداف بعرض 32 بتًا ضمنها)، أما المفسر
فيقصّر مخزنه، وينسخ مخزن الحرفي الثابت أولًا كما يفعل الإلحاق.
المتجه الذي أُفرغ يحتفظ بمخزنه، كما يفعل Vec في Rust. فـ اقتطع، وأزل_الأخير حتى الإفراغ،
وامسح_المتجه تخزن الطول الجديد، والصفر منه، وتحتفظ بالسعة، فتعيد الإلحاقات التالية استعمال المخزن.
وتحرر دالة الإتلاف المولدة مخزن المتجه حين تكون سعته أكبر من الصفر، لا حين يكون طوله كذلك؛ والمتجه الذي لم يخصص قط سعته صفر.
والحلقة التي تملأ متجهات من النصوص والمتجهات المتداخلة والدوال المغلقة وتفرغها 10^5 مرة لا تغير عدد البايتات الحية
في المخصص (tests/*/language/surface/vector_pop_rss_flat.sn).
وقت الترجمة، يشغّل التقييم MSIR نفسه في المفسر. فهو يسمح بالبدائيات التي تغير قيمة يملكها التقييم (الإلحاق، وتخزين
الطول) وبتحرير تخزين المفسر نفسه، فتستطيع شيفرة وقت_الترجمة بناء المتجهات والإزالة من آخرها واقتطاعها. أما الآثار
المرئية، مثل ذاكرة الآلة وبيئة المضيف، فتبقى ممنوعة.
ويخفض Stage0 المكتوب بلغة C البدائيات نفسها على ترويسة المتجه الخاصة به (bootstrap/src/llvm/llvm_builtins.c)، فيجوز
للمترجم ولوحدات المكتبة القياسية التي يستوردها استعمالها. فـ std.text.number يزيل أرقامه من الآخر، وstd.collections.vector يبني
عليها احتفظ_في_المتجه وأزل_المتكرر_المتتالي وأزل_عند وأزل_بالتبديل وأدرج_عند وامسح_المتجه، مقابلات
retain وdedup_by وremove وswap_remove وinsert وclear في Rust.
السعة
ترويسة المتجه ثلاث كلمات بحجم المؤشر: البيانات والطول والسعة (compiler/backend/layout/layout_resolution.sn).
والتخطيط واحد في x86_64 وarm64 وwasm32 (كلمات بعرض 32 بتًا) وLLVM (بحجم المؤشر في الأهداف بعرض 32 بتًا أيضًا). والسعة
هي عدد العناصر التي يتسع لها المخزن قبل أن يعيد الإلحاق تخصيصه. ويخصص حرفي المتجه ذو ن عناصر ن بالضبط،
كما يفعل vec![...] في Rust: تصدر x86_64 وarm64 حجزًا دقيقًا واحدًا قبل إلحاق العناصر، وتخصص wasm32 وLLVM
والمفسران ن مباشرة. وينمّي كل إلحاقٍ المخزنَ بسياسة واحدة
(compiler/runtime/vector_memory_policy.sn، سياسة_نمو_المتجه_لعنصر)، وهي قاعدة RawVec::MIN_NON_ZERO_CAP في
Rust لأول سعة غير صفرية:
| حجم العنصر | السعة الأولى |
|---|---|
بايت واحد (النص وط8 وص8 ومنطقي) |
8 |
| من 2 إلى 1024 بايتًا | 4 |
| فوق 1024 بايتًا | 1 |
وبعد ذلك يضاعف الإلحاق السعة، وينمّيها ألحق_من إلى max(2 × السعة, المطلوب) (أدناه). فيكون لمتجه من عدد
إلى أربعة أعداد صحيحة بُني بالإلحاق سعة 4، ويجعلها الإلحاق الخامس 8، وللحرفي [1, 2, 3] سعة 3 ويجعلها
أول إلحاق 6، ولنص من 3 بايتات بُني بالإلحاق سعة 8،
ويبدأ متجه سجلات من 1024 بايتًا بـ 4 ومتجه سجلات من 1025 بايتًا بـ 1، في كل خلفية وفي مفسر الآلة. ويبقى احجز_سعة
ومتجه_بسعة وقلص_المتجه والحرفي والنسخة بالضبط.
سعة(م)تقرأ كلمة السعة في الترويسة بصيغةعدد، كما تفعلالطولوcapفي Go.احجز_سعة(م, ن)(Vec::reserve_exactفي Rust) توفر مكانًا لـنعناصر إضافية. فإذا تجاوزالطول + نالسعة، أُعيد تخصيص المخزن لذلك العدد من العناصر بالضبط عبر خدمة التخصيص نفسها وتوقفات الفيض نفسها التي يستعملها نمو الإلحاق: تستعمل x86_64 وarm64 تعليمة الإلحاق بصيغة الحجز، وتستعمل wasm32 وLLVM إعادة التخصيص المحروسة نفسها. وهي تكتب في وسيطها الأول، وهو موضع قابل للتعديل، كما يفعل الإلحاق، ويفحصها SSF على أنها كتابة.ألحق_من(م, &المصدر, البداية, العدد)(Vec::extend_from_sliceفي Rust) تلحق مدى من العناصر. فإذا تجاوزالطول + العددالسعة، نما المخزن مرة واحدة إلىmax(الطول + العدد, السعة × 2, الحد_الأدنى)، وهي قاعدة النمو المُطفأ في السياسة (سعة_نمو_المتجه، كما فيRawVec::grow_amortizedفي Rust)، لا إلىالطول + العددبالضبط، فيكلف إلحاق المديات في حلقة O(1) مُطفأة لكل عنصر، كما يكلف الإلحاق. وكان النمو بالضبط يجعلكرر_النصفيstd.textيعيد التخصيص مرة لكل تكرار: 4095 إعادة تخصيص و134 MB من البايتات المطلوبة لـGET /largeفي Nahr (64 KiB). والمخزن الذي فيه متسع لا يُمس، فتحتفظ النسخة، التي تحجز طول المصدر بالضبط ثم تلحق فيه، بسعتها المضبوطة. وتنمو كل خلفية والمفسران كلاهما (مفسر الآلة وSULTANC_INTERPRETER=checked) بهذه الدالة الواحدة.- يبني
std.collections.vectorعليها:متجه_بسعة(Vec::with_capacityفي Rust)، واحجز_في_المتجه(Vec::reserveفي Rust؛ وحين ينمو، ينمو إلى ضعف السعة على الأقل، فتبقى تكلفة الحجز المتكرر مُطفأة (amortized))، وقلص_المتجه(تنتقل العناصر بترتيبها إلى مخزن بعددها بالضبط، والمتجه الفارغ يحرر مخزنه).
ويحتفظ المفسر بالسعة نفسها في كل مخزن (مخزن_مفسر.السعة): سعة سياسة النمو حين يُصنع المخزن من عناصر، وتنمو
بالإلحاق، وترتفع بالحجز، وتبقى مع الاقتطاع. ولذلك تجيب سعة كما تجيب في الملف التنفيذي. والعنصر 0 في المخزن
المفرغ يسمي بداية المخزن، وهي ما تحرره دالة الإتلاف المولدة. ويقارن tests/*/stdlib/behavior/vector_capacity.sn نتائج الشيفرة
الأصلية والمفسر، ويتحقق vector_capacity_rss_flat.sn من أن 10^5 دورة من متجه_بسعة واحجز_في_المتجه واقتطع(م, 0)
وإعادة الاستعمال، ومن أزل_الأخير حتى الإفراغ وقلص_المتجه، لا تغير عدد البايتات الحية في المخصص.
بيئات الدوال المغلقة
قيمة الدالة المغلقة هي عنوان بيئة في الكومة. وتبقى الدوال المغلقة قابلة للنسخ، لأن الواجهة الأمامية تصنف أنواع الدوال على أنها بسيطة، ولذلك تُعد مراجع البيئة (reference counting) بدل أن تكون مملوكة ملكية فردية. والنموذج هو زيادة مراجع سياقات الدوال المغلقة وإنقاصها في Swift (retain وrelease)، معبَّرًا عنهما عبر الدوال المولدة للملكية نفسها في MSIR التي تستعملها كل قيمة مملوكة أخرى.
التخطيط (خطط_بيئة_إغلاق_للاختيار، compiler/backend/target/selection_query_context.sn، وتشترك فيه x86_64 وarm64
وwasm32 وLLVM). ترويسة من 24 بايتًا في ثلاث خانات كل منها 8 بايتات تحمل مؤشر الشيفرة، وعدد المراجع، ومؤشر شيفرة
المُتلِف، تليها القيم الملتقطة محاذاةً. والإنشاء (بناء_إغلاق) يضبط العدد على واحد ويخزن المُتلِف. وكل قيمة دالة بيئة،
ومنها الدالة المسماة المستعملة قيمةً.
الملكية في MSIR. يعد يملك_تخزينا_للغراء نوع الدالة مالكًا لمرجع واحد، فيُتلف مالكو قيم الدوال، ومالكو المركبات
والمتجهات التي تحملها، كأي مالك آخر. والنوع الذي يحمل مراجع دوال مغلقة ولا شيء غيرها (هل_نوع_معدود_عادي_مسير: نوع
دالة، أو مركب من أعضاء بسيطة وقيم دوال) بسيط في نظر الواجهة الأمامية، فلا يسجل SSF نقلًا له. ولذلك يحافظ الخفض على
مرجع واحد لكل مالك (drops.sn، "Counted values"):
- كل موضع يأخذ الملكية يتلقى مرجعه الخاص. وهذه المواضع هي: التخزين في متغير محلي أو موضع، والوسيط (المعاملات تملك
قيمتها)، والإرجاع، وعضو المركب أو حمولة المنشئ، والعنصر الملحق، والالتقاط في دالة مغلقة، وربط المطابقة المعدود.
والقيمة الجديدة (نتيجة استدعاء، أو إنشاء دالة مغلقة، أو نقل سجله SSF) تُنقل، وأي قراءة أخرى تُنسخ ببدائية
احتفاظ_بقيمة(خفض_تعبير_مملوكا_إلى_مسير،احتفظ_بقراءة_معدودة_خفض). - المستدعى الجديد (
اصنع()(س)) يُتلف بعد الاستدعاء.
الدوال المولدة (ownership_glue.sn). يصير احتفاظ_بقيمة دالة النسخ المولدة للنوع، ودالة النسخ المولدة لنوع الدالة
تزيد عدد المراجع واحدًا (زيادة_مراجع_إغلاق). أما دالة الإتلاف المولدة لنوع الدالة فهي:
إذا (إنقاص_مراجع_إغلاق(د)) { مدمر_بيئة_إغلاق(د)(); حرر(د) }
ينقص إنقاص_مراجع_إغلاق عدد المراجع واحدًا ويجيب هل بقي منها شيء. ويوجه مدمر_بيئة_إغلاق خانة الشيفرة في البيئة إلى
مُتلِفها ويعيد البيئة دالةً مغلقة من نوع () -> ()، فيشغّل الاستدعاء غير المباشر العادي المُتلِفَ بتلك البيئة. ومُتلِف
الدالة F المنشأة من دالة مغلقة دالةٌ مولدة بأنواع التقاطات F وبلا معاملات: تقرأ عمليات استخراج الالتقاطات فيها
البيئة الآيلة إلى الإتلاف، وتتلف الالتقاطات المملوكة (قيم الدوال المغلقة التي التقطتها دالة مغلقة). والبيئات التي لا
تملك شيئًا تتشارك مُتلِفًا فارغًا واحدًا. ويعين تمرير الدوال المولدة مُتلِفًا لكل إنشاء (بناء_إغلاق_مسير.المدمر) قبل أن
ترى أي خلفية البرنامج. وهذه البدائيات الثلاث هي كل ما على الخلفيات. فكل خلفية أصلية وLLVM وWebAssembly تخفضها في بضع تعليمات
على الترويسة. وينفذ المفسر البدائيات الثلاث نفسها (compiler/interpreter/execution/primitives.sn) على مقابض الدوال
المغلقة لديه، ويعد المراجع كما تفعل الترويسة الأصلية.
أجسام الدوال المغلقة. يُفحص جسم الدالة المغلقة ويُخفض على أنه جسم دالة مستقل، كما يفحص rustc استعارات الدالة
المغلقة جسمَ MIR منفصلًا. يحلله SSF بحالة تدفق جديدة (حلل_جسم_إغلاق_لتدفق،
compiler/frontend/ssf/semantic_pipeline.sn). معاملاته متغيرات محلية مملوكة، والتقاطاته (القيم البسيطة التي تحملها
البيئة) متغيرات محلية مهيأة، وقيمة الجسم هي ما يُعاد. ولذلك يُرفض الجسم الذي يستعمل معاملًا منقولًا
(SULTANC-OWNERSHIP-USE-AFTER-MOVE)، وتُسجل عمليات النقل التي يجريها. ويعطي الخفض الجسم إطار الإتلاف نفسه الذي يعطيه
للدالة (اخفض_إغلاق_إلى_مسير، compiler/msir/lowering/expressions.sn): فالمعاملات التي لا يستهلكها، ومنها قيم الدوال،
تُتلف عند رجوعه، أما الالتقاطات فلا، لأن مُتلِف البيئة يملكها. ويحافظ tests/*/language/surface/closure_drop_rss_flat.sn
على ثبات البايتات الحية في المخصص عبر 10^5 دورة.
تكلفة التصنيف. يسأل الخفض هل يملك النوع تخزينًا، مع الدوال المغلقة وبدونها، عند كل تخزين ووسيط وإتلاف. ويخزن السجل
الإجابة مؤقتًا لكل معرف نوع (TypeID) (تصنيفات_ملكية_مسير، ويملؤها املأ_تصنيفات_ملكية_مسير مرة واحدة قبل خفض أجسام الدوال،
ownership_glue.sn) بدل المرور على بنية النوع عند كل سؤال.
القيم المؤقتة
نتيجة الاستدعاء قيمة مؤقتة لا يملكها أي متغير محلي، وكذلك حرفي المتجه المستعمل تعبيرًا ([أ, ب][ي]،
الطول([أ, ب])؛ انظر نظام الأنواع). وتنطبق عليها لواحق الحقل والصف
والفهرس والاستدعاء (اصنع().الاسم، اصنع().الداخلي.ن، كلمات()[1]، اصنع().المعالج(3)، جامع(1)(2))، ويتبع الخفض
قواعد Rust للقيم المؤقتة (هل_أصل_مؤقت_للخفض، أسقط_بقية_مؤقت_خفض، compiler/msir/lowering/drops.sn):
- العضو المالك المقروء من قيمة مؤقتة يُنقل إلى الخارج، وتُتلف الأعضاء المالكة الأخرى للقيمة المؤقتة. ويتلفها Rust في نهاية التعليمة المحيطة؛ ولا شيء يستطيع تسمية القيمة المؤقتة بعد القراءة، فإتلافها بعدها مباشرة مكافئ لذلك.
- العضو البسيط يُنسخ، وتُتلف القيمة المؤقتة كلها.
- عنصر المتجه المؤقت يُنسخ (فالمتجه لا يسمح بنقل عنصر منه، وهو E0507 في Rust)، ويُتلف المتجه.
- عضو القيمة المؤقتة جديد هو نفسه: فالعضو المعدود (دالة مغلقة) يُنقل بدل أن يُحتفظ به، والمستدعى الجديد يُتلف بعد
الاستدعاء. وتعامله المطابَقات وبدائيات القراءة (
الطول،استنسخ) وعروض الشرائح كما تعامل نتائج الاستدعاءات.
ويغطيها tests/*/language/surface/temporary_members.sn (تتفق الشيفرة الأصلية والمفسر) وtemporary_members_rss_flat.sn
(البايتات الحية ثابتة عبر 10^5 دورة).
وحرفي السجل المستعمل تعبيرًا (مجموع({ س: 1, ص: 2 })، أرجع { .. };، ن := { .. };) قيمة مؤقتة كذلك: يأخذ نوعه من
سياقه، وتُنقل قيم حقوله إليه كما في دع س: ن = {..};.
ويُقبل &ت لقيمة مؤقتة (حرفي أو نتيجة استدعاء) حجةً لاستدعاء: طول_نص(&«xy»)، مجموع(&[1, 2])،
فرق(&{ أ: 1, ب: 2 })، د(&اصنع()). تُخزن القيمة في خانة تملكها الكتلة المحيطة (تُتلف عند مخارج الكتلة،
اخفض_عنوان_مؤقت_إلى_مسير)، ويستعير الاستدعاء الخانة. ولا يُقبل إلا حين لا يستطيع ناتج المستدعى حمل مرجع
(يسمح_بعنوان_مؤقت_داخلي)، فلا تعيش الاستعارة بعد الخانة؛ وفي غير ذلك يبقى SULTANC-TYPE-ADDRESS-OF-NON-LVALUE.
ويغطيها tests/*/language/surface/record_literal_expressions.sn وtemporary_address_arguments.sn
وreject_temporary_address_escape.sn.
الوصول إلى سجل الأنواع
يملك السجل كل نوع_مقنن. يحلل القراء الهوية بـ معرف_نوع_موجود_عند أو معرف_نوع_أساس_عند، ثم يطابقون
(*سجل).الأنواع[id] في موضعه. ويُوصل إلى بنية الأنواع المسماة عبر مقبض بسيط، موضع_تعريف_بنية، وهو خانة قالب أو خانة
نسخة، ويقرأ حقيقة_بنية_عند الحقائق من التعريف المستعار. أما الإنشاء العام (ضمن_مثيلات_نوع_داخل، حل_نوع_مسمى_بحجج)
فيعدّل السجل، ولذلك يعمل على نسخ من القالب والوسائط التي يحتاجها.
قيود شيفرة المترجم
يترجم Stage0 (bootstrap/**، ويملكه مشرفو الإقلاع الذاتي) compiler/** ووحدات المكتبة القياسية التي يستوردها، فتبقى تلك الشيفرة ضمن
لغة Stage0. والقيود، مع أسبابها، هي:
- لا بدائية نسخ، ولا انعكاس للأنواع (reflection)، ولا سمات (traits). النسخ دوال مكتوبة يدويًا.
- لا إرجاع للمراجع. يرفض Stage0 أي دالة تعيد مرجعًا (
SULTANC-BORROW-REFERENCE-ESCAPE)، حتى المرجع المشتق من معامل، وحتى داخل اختياري. ولذلك تعيد دوال الوصول هويات أو مقابض، ويستعير المستدعون الخانة. ولا يمكن تخزين المراجع في حرفيات السجلات ولا إسنادها إلى متغيرات محلية قابلة للتعديل. - الأنماط. تُعد أنماط المنشئات المتداخلة شاملة إلى عمق مستوى واحد (
نتيجة.نجح(اختياري.لا_شيء)بجانبنتيجة.نجح(اختياري.موجود(س)))، والتداخل الأعمق يحتاج ذراعًا شاملة. ولا يجوز للبدائل أن تربط أسماء. - ليست في لغة Stage0: الدوال المغلقة (حرفيات
دالة(…) -> …)، ووقت_الترجمة، والتعليقات التوضيحية (@…)، والصفوف، والشرائح ([] T)، والاستيراد الانتقائي (من … استورد)، والأسماء البديلة للاستيراد، وإرجاعالرئيسيةنتيجةً، وقراءة حقل من سجل مؤقت بعمق أكثر من عضو واحد حين يملك ذاكرة. - الصياغة. لا يُحلَّل
(**س)، فاكتب(*(*س)). ويجب كتابةبدّل(*ر, س)على صورةبدّل((*ر), س).
ويترجم Stage0 أيضًا ما أضافته الواجهة الأمامية ذاتية الاستضافة بعد كتابته، بالدلالات نفسها: ؟؛ وإنهيار منهيًا
لمساره؛ وثوابت الوحدات ومتغيراتها (دع في المستوى الأعلى، وتُطوى وقت الترجمة؛ وتحتفظ بياناتها الثابتة بسعة 0)؛
واخرج_من_حلقة وتابع_حلقة ولكل؛ وأي تعبير في حرفي متجه وبعد صندوق؛ وأنماط الحرفيات والمدى والبدائل في أي عمق؛
وحرفيات الأعداد العائمة وحسابها وتحويلاتها (llvm.fpto[su]i.sat)؛ والدوال الجوهرية للحساب الملتف؛ وسعة واحجز_سعة
وأزل_الأخير واقتطع؛ وحرر؛ وأعضاء نتائج الاستدعاءات وعناصرها واستدعاءاتها، والاستدعاءات عبر حقول قيمتها دوال؛
والمقاطع التي هي كلمات مفتاحية في مسارات الاستيراد (std.math.float)؛ و>>>؛ والأسماء العربية لحزمة المكتبة القياسية
(المكتبة.أخطاء.failure)؛ وفواصل Unicode؛ والاستعارة المشتركة لحرفي نصي (&"xy"، &«xy») المخزن في قيمة مؤقتة تبقى
بعد الاستدعاء (tests/*/language/stage0/literal_borrow.sn)؛ والاستعارة المشتركة لنتيجة استدعاء وسيطًا لاستدعاء
(f(&g(x))، ومتداخلة أيضًا)، وتُخزَّن نتيجتها في قيمة مؤقتة قبل الاستدعاء وتُسقَط، فيُحرَّر نصها أو متجهها، بعد عودة ذلك
الاستدعاء مباشرة (tests/*/language/stage0/call_borrow.sn). وتُقبل أذرع المطابقة التي لا يمكن الوصول إليها، ويحذّر منها المترجم ذاتي
الاستضافة.
القيود
- العروض المشتقة من مراجع مُعادة. العرض المحمول عبر متغير محلي من نوع مرجع مشتق من معامل حصري لا ينتهي حين يُمرر
ذلك المعامل لاحقًا بصيغة
&متغير. ولا تنتهي إلا العروض التي يكون حاملها هو المعامل نفسه. ويمنع Stage0 إرجاع المراجع، فلا تستطيع شيفرة المترجم إنشاء مثل هذا المتغير المحلي؛ أما برامج المستخدمين التي يترجمها Stage2 فتستطيع. - الدوال المغلقة. لا يقبل SSF إلا الالتقاطات البسيطة. والدالة المغلقة التي تحتاج قيمة مملوكة تأخذها معاملًا، فيتلفها جسم الدالة المغلقة، أو تلتقط قيمة دالة مغلقة، فتُعد ويتلفها مُتلِف البيئة.