من أحد المترجمين: تم نشر هذا المنشور على مدونة المؤلف في 15 مارس 2018. مع تطور اللغة ، قد يكون بناء الجملة الخاص بها مختلفًا في الوقت الحالي. كل ما تم وصفه يتعلق بـ Zig 0.2.0 ، الإصدار الحالي للغة هو Zig 0.3.0.
اتصلت بمؤلف المنشور ، وتفضل بتقديم رابط إلى المستودع بالنسخة الحالية من مصادر المشروع على Zig 0.3.0
مرحبا دعنا نكتب مترجم Brainfuck! "لماذا؟" "قد تسأل ، لكنك لن تجد الجواب هنا."
سأفعل ذلك على
منعرج .

منعرج هو ...
... لغة برمجة جديدة. لا يزال في مرحلة تجريبية ويتطور بسرعة. إذا كنت قد شاهدت الرمز Zig من قبل ، فقد يبدو الرمز في هذا المنشور مختلفًا تمامًا عنك. إنه مختلف حقًا! تم إصدار Zig 0.2.0 لتوه ، متزامنًا مع إصدار
LLVM 6 قبل بضعة أسابيع ، ويتضمن العديد من تغييرات بناء الجملة وتحسينات اللغة العامة. في الغالب ، تم استبدال "تعويذات" كثيرة بكلمات رئيسية. انظر
هنا للحصول على شرح أعمق لجميع التغييرات!
تم تصميم Zig بحيث
يكون قابلاً للقراءة وبديهية نسبيًا لأولئك الذين لديهم دراية باللغات المترجمة والمكتوبة مثل C و C ++ و Rust في بعض النقاط.
تم تجميع الشفرة واختبارها باستخدام Zig 0.2.0 ، والذي يتوفر الآن من
خلال قنوات مختلفة ، بما في ذلك homebrew ، إذا كنت تستخدم OSX: brew install zig.
لنبدأ
لمعرفة كيفية عمل Brainfuck ، انظر
هنا . لا يوجد شيء تقريبًا لتتعلمه هناك ، لكنها لغة
تورنج كاملة ، مما يعني أنه يمكنك كتابة
أي شيء عليها.
لقد قمت بنشر الكود
هنا ، في حال كنت تريد رؤية المنتج النهائي أو الالتزامات المبكرة.
منعرج لغة مترجمة. عند ترجمة برنامج ، يجب أن يكون للثنائي الناتج (إذا كنت تقوم بتجميع ثنائي قابل للتنفيذ ، وليس مكتبة) وظيفة رئيسية تحدد نقطة الإدخال.
لذلك ...
// main.zig fn main() void { }
... وابدأ ...
$ zig build-exe main.zig
... يعطي ...
/zig/std/special/bootstrap.zig:70:33: error: 'main' is private /zigfuck/main.zig:2:1: note: declared here
يجب الإعلان عن الجمهور على أنه عام ليكون مرئيًا خارج الوحدة النمطية ...
// main.zig pub fn main() void { }
دع برنامج brainfuck يستخدم مجموعة من 30000 بايت كذاكرة ، وسوف أصنع مثل هذه المجموعة.
// main.zig pub fn main() void { const mem: [30000]u8; }
يمكنني إعلان ثابت (ثابت) أو متغير (فار). هنا ، أعلنت mem كصفيف من 30000 بايت (u) غير موقعة (8 بت).
هذا لا يجمع.
/main.zig:3:5: error: variables must be initialized
سيتم تجميع برنامج C مكافئ بشكل طبيعي: يمكنني إعلان متغير دون التهيئة ، لكن Zig يجبرني على اتخاذ قرار الآن ، في الوقت الذي يتم فيه الإعلان عن المتغير. قد لا أهتم بما سيتم كتابته فيه ، لكن يجب أن أشير إلى ذلك صراحة. سأفعل ذلك عن طريق تهيئة المتغير بقيمة غير محددة (غير محددة).
// main.zig pub fn main() void { const mem: [30000]u8 = undefined; }
تهيئة متغير بقيمة غير محددة لا يعطي أي ضمانات حول قيمة المتغير في الذاكرة. هذا هو نفس إعلان متغير غير مهيأ في C باستثناء أنك تحتاج إلى الإشارة صراحةً إلى ذلك.
لكن ربما لا يهمني كيفية تهيئة هذه الذاكرة. ربما أريد الحصول على ضمان بأن الأصفار أو بعض القيمة التعسفية مكتوبة هناك. في هذه الحالة ، أود أيضًا أن أذكر هذا بوضوح:
// main.zig pub fn main() void { const mem = []u8{0} ** 30000; }
قد يبدو غريباً ، لكن ** هو المشغل المستخدم لتوسيع المصفوفات. أعلن مجموعة من 0 بايت ، ثم قم بتوسيعه إلى 30،000 ، وأحصل على قيمة التهيئة النهائية البالغة 30،000 بايت. هذه العملية تحدث مرة واحدة ،
في وقت الترجمة . يعد comptime أحد أفكار Zig الرائعة ، وسأعود إليه في أحد الوظائف التالية.
الآن دعنا نكتب برنامجًا على brainfuck لا يفعل شيئًا سوى زيادة فتحة الذاكرة الأولى خمس مرات!
pub fn main() void { const mem = []u8{0} ** 30000; const src = "+++++"; }
في منعرج ، سلاسل هي صفائف بايت. لا ينبغي أن أعلن src كصفيف بايت ، لأن المترجم يتضمن هذا. هذا اختياري ، ولكن إذا كنت ترغب في ذلك فمن الممكن:
const src: [5]u8 = "+++++";
هذا سوف تجميع غرامة. ومع ذلك ، هذا:
const src: [6]u8= "+++++";
لن يكون.
main.zig:5:22: error: expected type '[6]u8', found '[5]u8'
ملاحظة أخرى: بما أن الأوتار صفائف ، فهي لا تنتهي بصفر. ومع ذلك ، يمكنك إعلان سلسلة منتهية بقيمة خالية. C. كحرف ، سيبدو كما يلي:
c"Hello I am a null terminated string";
من أجل الصالح العام ...
أريد أن أفعل
شيئًا مع كل شخصية في السلسلة. أستطيع أن أفعل ذلك! في بداية main.zig ، أقوم باستيراد بعض الوظائف من المكتبة القياسية:
const warn = @import("std").debug.warn;
الاستيراد ، مثل كل شيء يبدأ فعليًا بعلامة @ ، هو
وظيفة مترجم مدمجة . هذه الميزات متوفرة دائمًا على مستوى العالم. يعمل الاستيراد هنا على غرار جافا سكريبت - يمكنك استيراد أي شيء عن طريق الحفر في مساحة الاسم واستخراج أي وظائف أو متغيرات متاحة للجمهور منه. في المثال أعلاه ، أقوم باستيراد وظيفة التحذير مباشرةً وأعينها ، فجأة ، إلى ثابت التحذير. الآن يمكن أن يسمى. هذا نمط شائع: نحن نستورد مباشرة من مساحة الاسم std ثم ندعو std.debug.warn () أو نسنده إلى متغير warn. يبدو مثل هذا:
const std = @import("std"); const warn = std.debug.warn;
const warn = @import("std").debug.warn; // main.zig pub fn main() void { const mem = []u8{0} ** 30000; const src = "+++++"; for (src) |c| { warn("{}", c); } }
أثناء تصحيح الأخطاء والتطوير الأولي والاختبار ، أريد فقط طباعة شيء ما على الشاشة. منعرج هو
عرضة للخطأ ، و stdout هو أيضا عرضة للخطأ. لا أرغب في القيام بذلك الآن ، ويمكنني الطباعة مباشرةً إلى stderr باستخدام التحذير ، الذي استوردناه من المكتبة القياسية.
التحذير يأخذ سلسلة منسقة ، مثل printf في C! سيتم طباعة الكود أعلاه:
4343434343
43 هو رمز الحرف ascii +. يمكنني أيضا أن أكتب:
warn("{c}", c);
واحصل على:
+++++
لذلك ، قمنا بتهيئة مساحة الذاكرة ، وكتبنا البرنامج. الآن نحن ندرك اللغة نفسها. سأبدأ بـ + ، واستبدل جسم الحلقة for بتبديل:
for (src) |c| { switch(c) { '+' => mem[0] += 1 } }
أحصل على خطأين:
/main.zig:10:7: error: switch must handle all possibilities switch(c) { ^ /main.zig:11:25: error: cannot assign to constant '+' => mem[0] += 1 ^
بالطبع ، لا يمكنني تعيين قيمة جديدة لمتغير ، وهو ثابت! يحتاج ميم ليكون متغير ...
var mem = []u8{0} ** 30000;
كما هو الحال مع الأخطاء الأخرى ، يجب أن يعرف إنشاء
المحول الخاص بي ما يجب القيام به إذا لم يكن الرمز + ، حتى إذا لم يكن هناك شيء يجب القيام به. في حالتي ، هذا هو بالضبط ما أريد. لقد ملأت هذه الحالة مع كتلة فارغة:
for (src) |c| { switch(c) { '+' => mem[0] += 1, else => {} } }
الآن يمكنني تجميع البرنامج. استدعاء تحذير في النهاية وتشغيل:
const warn = @import("std").debug.warn; pub fn main() void { var mem = []u8{0} ** 30000; const src = "+++++"; for (src) |c| { switch(c) { '+' => mem[0] += 1, else => {} } } warn("{}", mem[0]); }
أحصل على الرقم 5 المطبوع في
stderr ، كما كنت أتوقع.
دعنا ننتقل ...
وبالمثل ، نحن ندعم.
switch(c) { '+' => mem[0] += 1, '-' => mem[0] -= 1, else => {} }
لاستخدام> و <، تحتاج إلى استخدام متغير إضافي ، والذي يكون بمثابة "مؤشر" في الذاكرة التي خصصتها لبرنامج brainfuck للمستخدم.
var memptr: u16 = 0;
نظرًا لأن 16 بت غير موقّع يمكن أن يصل إلى 65535 كحد أقصى ، يكفي فهرسة 30،000 بايت من مساحة العنوان.
في الواقع ، سيكون 15 بت كافية بالنسبة لنا ، مما يسمح لنا بمعالجة 32767 بايت. يسمح Zig لأنواع بعرض مختلف ، ولكن ليس u15 حتى الآن.
يمكنك فعلًا إجراء u15 بهذه الطريقة:
const u15 = @IntType(false, 15):
يُقترح أن يكون أي نوع [iu] \ d + صالحًا كنوع صحيح.
الآن بدلاً من استخدام mem [0] ، يمكنني استخدام هذا المتغير.
'+' => mem[memptr] += 1, '-' => mem[memptr] -= 1,
<and> ببساطة قم بزيادة وتقليل هذا المؤشر.
'>' => memptr += 1, '<' => memptr -= 1,
عظيم يمكننا كتابة برنامج حقيقي الآن!
تحقق 1،2،3
منعرج لديه محرك اختبار مدمج. في أي مكان في أي ملف يمكنني كتابة كتلة اختبار:
test "Name of Test" { // test code }
وقم بإجراء الاختبار من سطر الأوامر: zig test $ FILENAME. بقية كتل الاختبار هي نفس الشفرة العادية.
لنلقِ نظرة على هذا:
// test.zig test "testing tests" {} zig test test.zig Test 1/1 testing tests...OK
بالطبع ، اختبار فارغ لا طائل منه. يمكنني استخدام تأكيد لتأكيد تنفيذ الاختبارات في الواقع.
const assert = @import("std").debug.assert; test "test true" { assert(true); } test "test false" { assert(false); }
zig test test.zig "thing.zig" 10L, 127C written :!zig test thing.zig Test 1/2 test true...OK Test 2/2 test false...assertion failure [37;1m_panic.7 [0m: [2m0x0000000105260f34 in ??? (???) [0m [37;1m_panic [0m: [2m0x0000000105260d6b in ??? (???) [0m [37;1m_assert [0m: [2m0x0000000105260619 in ??? (???) [0m [37;1m_test false [0m: [2m0x0000000105260cfb in ??? (???) [0m [37;1m_main.0 [0m: [2m0x00000001052695ea in ??? (???) [0m [37;1m_callMain [0m: [2m0x0000000105269379 in ??? (???) [0m [37;1m_callMainWithArgs [0m: [2m0x00000001052692f9 in ??? (???) [0m [37;1m_main [0m: [2m0x0000000105269184 in ??? (???) [0m [37;1m??? [0m: [2m0x00007fff5c75c115 in ??? (???) [0m [37;1m??? [0m: [2m0x0000000000000001 in ??? (???) [0m
سقط الاختبار. استخدم الأمر التالي لإنتاج الخطأ:
./zig-cache/test
تتبع المكدس على الخشخاش لا يزال قيد التطوير.لاختبار ذلك بكفاءة ، أحتاج إلى تقسيمه إلى أجزاء. لنبدأ بهذا:
fn bf(src: []const u8, mem: [30000]u8) void { var memptr: u16 = 0; for (src) |c| { switch(c) { '+' => mem[memptr] += 1, '-' => mem[memptr] -= 1, '>' => memptr += 1, '<' => memptr -= 1, else => {} } } } pub fn main() void { var mem = []u8{0} ** 30000; const src = "+++++"; bf(src, mem); }
يجب أن يبدو للعمل ، أليس كذلك؟
لكن ...
/main.zig:1:29: error: type '[30000]u8' is not copyable; cannot pass by value
يوصف هذا في https://github.com/zig-lang/zig/issues/733 .
منعرج صارم حول هذا الموضوع. لا يمكن تمرير الأنواع المعقدة وكافة الكائنات التي يمكن تغيير حجمها حسب القيمة. هذا يجعل تخصيص المكدس متوقعًا ومنطقيًا ، ويتجنب النسخ غير الضروري. إذا كنت تريد استخدام دلالات النقل من حيث القيمة في البرنامج ، فيمكنك تنفيذها بنفسك باستخدام استراتيجية التخصيص الخاصة بك ، ولكن اللغة نفسها لا تدعم هذا في ظل الظروف العادية.
الطريقة الطبيعية حول هذا القيد هي تمرير مؤشر بدلاً من قيمة (تمرير حسب المرجع). منعرج يستخدم استراتيجية مختلفة ، وشرائح. الشريحة عبارة عن مؤشر بطول متصل به ومع فحص للسقوط في الحدود. يبدو بناء الجملة في توقيع الوظيفة كما يلي:
fn bf(src: []const u8, mem: []u8) void { ... }
وعند استدعاء الوظيفة ، يبدو الأمر كما يلي:
bf(src, mem[0..mem.len]);
لاحظ أنني قمت بتحديد الحد الأعلى ببساطة عن طريق الإشارة إلى طول الصفيف. يوجد تدوين مختصر لمثل هذه الحالات:
bf(src, mem[0..]);
الآن يمكنني البدء في كتابة الاختبارات التي تختبر وظيفة bf () مباشرة. سأضيف وظائف الاختبار إلى نهاية الملف الآن ...
test "+" { var mem = []u8{0}; const src = "+++"; bf(src, mem[0..]); assert(mem[0] == 3); }
آخذ صفيف mem من بايت واحد ثم تحقق مما يجب أن يحدث (يتم زيادة البايت ثلاث مرات). إنه يعمل!
Test 1/1 +...OK
يتم فحص "-" بنفس الطريقة:
test "-" { var mem = []u8{0}; const src = "---"; bf(src, mem[0..]); assert(mem[0] == 253); }
لا يعمل! عندما أحاول طرح 1 من 0 ، أحصل ...
Test 2/2 -...integer overflow
mem عبارة عن صفيف من وحدات البايت غير الموقعة ، ويؤدي طرح 1 من 0 إلى حدوث تجاوز سعة. مرة أخرى ، يجعلني Zig إعلان ما أريد صراحة. في هذه الحالة ، لا داعي للقلق بشأن تجاوز السعة ، في الواقع ، أريد أن يحدث ذلك ، لأننا نتعامل مع
وحدات حسابية ، وفقًا
لمواصفات brainfuck . هذا يعني أن تقليل الخلية التي تحتوي على الرقم 0 سوف يعطيني 255 ، وزيادة قدرها 255 سوف تعطيني 0.
لدى Zig العديد من العمليات الحسابية الإضافية التي تقدم
دلالات "الالتفاف" المضمون .
'+' => mem[memptr] +%= 1, '-' => mem[memptr] -%= 1,
هذا يحل مشكلة الفائض بأكمله ويفعل ما كنت أتوقعه.
لاختبار <و> ، أتصفح صفيفًا صغيرًا وأتحقق من قيمة الخلية المتزايدة:
test ">" { var mem = []u8{0} ** 5; const src = ">>>+++"; bf(src, mem[0..]); assert(mem[3] == 3); }
و ...
test "<" { var mem = []u8{0} ** 5; const src = ">>>+++<++<+"; bf(src, mem[0..]); assert(mem[3] == 3); assert(mem[2] == 2); assert(mem[1] == 1); }
في الحالة الأخيرة ، يمكنني مباشرة مقارنة النتيجة مع مجموعة ثابتة باستخدام ...
const mem = std.mem;
أذكر أنني قد استوردت بالفعل الأمراض المنقولة جنسيا. في المثال أدناه ، أستخدم mem.eql في مساحة الاسم هذه:
test "<" { var storage = []u8{0} ** 5; const src = ">>>+++<++<+"; bf(src, storage[0..]); assert(mem.eql(u8, storage, []u8{ 0, 1, 2, 3, 0 })); }
... وتذكر ، سلسلة الأحرف ، فهذه مجرد صفائف u8 في شكل متعرج ، ويمكنني أن أضع فيها الحرفيات السداسية عشرية ، أي سوف تعمل الكود التالي بنفس الطريقة!
assert(mem.eql(u8, storage, "\x00\x01\x02\x03\x00"));
أضف "."! ببساطة يطبع كحرف قيمة البايت في الخلية التي يشير إليها المؤشر. أنا أستخدم تحذير الآن ، ولكن في وقت لاحق سأستبدلها بـ stdout. هذا من السهل القيام به من الناحية المفاهيمية ، ولكنه مشوش إلى حد ما في التنفيذ. سأفعل ذلك لاحقا!
'.' => warn("{c}", storage[memptr]),
دورات
[و] - السحر يبدأ هنا ....
[- إذا كانت قيمة الخلية الحالية هي صفر ، تخطي الخطوات إلى قوس الإغلاق دون تنفيذ التعليمات البرمجية.
] - إذا كانت قيمة الخلية الحالية ليست صفرية ، فارجع إلى شريحة الفتح وقم بتنفيذ الشفرة مرة أخرى.
هذه المرة سأبدأ باختبار ، سأختبرها معًا (من الواضح أنه ليس من المنطقي اختبارها بشكل منفصل). حالة الاختبار الأولى - يجب أن تكون خلية التخزين [2] فارغة ، على الرغم من أن الحلقة يجب أن تزيدها إذا بدأت:
test "[] skips execution and exits" { var storage = []u8{0} ** 3; const src = "+++++>[>+++++<-]"; bf(src, storage[0..]); assert(storage[0] == 5); assert(storage[1] == 0); assert(storage[2] == 0); }
وسوف أقوم بإنشاء فراغات لبيان التبديل:
'[' => if (storage[memptr] == 0) { }, ']' => if (storage[memptr] == 0) { },
ماذا تفعل الآن؟ يمكنك استخدام نهج ساذج. أنا فقط زيادة مؤشر src حتى أجده]. لكن لا يمكنني استخدام حلقة for zig لهذا ، فقد تم إنشاؤها فقط للتكرار من خلال المجموعات ، دون فقد عناصرها. البنية المناسبة هنا هي بينما:
كان:
var memptr: u16 = 0; for (src) |c| { switch(c) { ... } }
أصبح ...
var memptr: u16 = 0; var srcptr: u16 = 0; while (srcptr < src.len) { switch(src[srcptr]) { ... } srcptr += 1; }
الآن يمكنني إعادة تعيين مؤشر srcptr في منتصف الكتلة ، سأقوم بذلك:
'[' => if (storage[memptr] == 0) { while (src[srcptr] != ']') srcptr += 1; },
يرضي هذا الاختبار "[] يتخطى تنفيذ التعليمات البرمجية ويخرج"
يرضي هذا الاختبار "[] يتخطى التنفيذ ويخرج" ، على الرغم من أنه غير موثوق به تمامًا ، كما سنرى.
ماذا عن إغلاق الأقواس؟ أعتقد أنه يمكن كتابتها ببساطة عن طريق القياس:
test "[] executes and exits" { var storage = []u8{0} ** 2; const src = "+++++[>+++++<-]"; bf(src, storage[0..]); assert(storage[0] == 0); assert(storage[1] == 25); } ']' => if (storage[memptr] != 0) { while (src[srcptr] != '[') srcptr -= 1; },
يمكنك أن ترى ما يحدث ... إن الحل الساذج ذو القوسين له عيب قاتل وينكسر تمامًا على الحلقات المتداخلة. النظر في ما يلي:
++>[>++[-]++<-]
يجب أن تكون النتيجة {2 ، 0} ، لكن الشريحة المفتوحة الأولى تنتقل ببساطة بغباء إلى أول شريحة إغلاق ، وكل شيء يصبح فوضويًا. تحتاج إلى القفز إلى شريحة الإغلاق التالية في نفس مستوى التعشيش. من السهل إضافة عداد عمق وتتبعه وأنت تتحرك للأمام على طول الخط. نحن نفعل ذلك في كلا الاتجاهين:
'[' => if (storage[memptr] == 0) { var depth:u16 = 1; srcptr += 1; while (depth > 0) { srcptr += 1; switch(src[srcptr]) { '[' => depth += 1, ']' => depth -= 1, else => {} } } }, ']' => if (storage[memptr] != 0) { var depth:u16 = 1; srcptr -= 1; while (depth > 0) { srcptr -= 1; switch(src[srcptr]) { '[' => depth -= 1, ']' => depth += 1, else => {} } } },
والاختبارات ذات الصلة: لاحظ أن src في كلا الاختبارين يتضمن حلقة داخلية.
test "[] skips execution with internal braces and exits" { var storage = []u8{0} ** 2; const src = "++>[>++[-]++<-]"; try bf(src, storage[0..]); assert(storage[0] == 2); assert(storage[1] == 0); } test "[] executes with internal braces and exits" { var storage = []u8{0} ** 2; const src = "++[>++[-]++<-]"; try bf(src, storage[0..]); assert(storage[0] == 0); assert(storage[1] == 2); }
بشكل منفصل ، لاحظ [-] - تعبير المصطلح "brainfuck" ، وهذا يعني "صفر هذه الخلية". يمكنك أن ترى أنه لا يهم القيمة التي كانت تملكها الخلية في البداية ، فسوف يتم إنقاصها حتى تصل إلى الصفر ، ثم سيستمر التنفيذ.
مسار سيئ الحظ
لم أعول على احتمال كسر البرنامج على فرنك بلجيكي. ماذا يحدث إذا قمت بتقديم برنامج إدخال غير صحيح لمترجم شفهي الخاص بي؟ على سبيل المثال ، ببساطة [بدون شريحة إغلاق ، أو <، والتي تتجاوز على الفور مجموعة الذاكرة؟ (يمكنني التفاف مؤشر الذاكرة ، لكن من الأفضل اعتبار هذا خطأ).
سوف أتطلع إلى الأمام قليلاً وأشرح جميع الاختلافات في الكود. سوف أضع وظيفة مترجم bf في ملف منفصل وأيضًا أضع وظيفة findBack و questForward في وظائفي الصغيرة.
const warn = @import("std").debug.warn; const sub = @import("std").math.sub; fn seekBack(src: []const u8, srcptr: u16) !u16 { var depth:u16 = 1; var ptr: u16 = srcptr; while (depth > 0) { ptr = sub(u16, ptr, 1) catch return error.OutOfBounds; switch(src[ptr]) { '[' => depth -= 1, ']' => depth += 1, else => {} } } return ptr; } fn seekForward(src: []const u8, srcptr: u16) !u16 { var depth:u16 = 1; var ptr: u16 = srcptr; while (depth > 0) { ptr += 1; if (ptr >= src.len) return error.OutOfBounds; switch(src[ptr]) { '[' => depth += 1, ']' => depth -= 1, else => {} } } return ptr; } pub fn bf(src: []const u8, storage: []u8) !void { var memptr: u16 = 0; var srcptr: u16 = 0; while (srcptr < src.len) { switch(src[srcptr]) { '+' => storage[memptr] +%= 1, '-' => storage[memptr] -%= 1, '>' => memptr += 1, '<' => memptr -= 1, '[' => if (storage[memptr] == 0) srcptr = try seekForward(src, srcptr), ']' => if (storage[memptr] != 0) srcptr = try seekBack(src, srcptr), '.' => warn("{c}", storage[memptr]), else => {} } srcptr += 1; } }
هذا يجعل التبديل أسهل بكثير ، في رأيي ، ابحث عن الأمام والسعي إلى الخلف وأعمل بشكل متشابه للغاية ، وقد أميل إلى إعادة تشكيلها في شيء أكثر ذكاءً وأكثر إحكاما ، لكن في النهاية يفعلون أشياء مختلفة ويتعاملون مع الأخطاء أيضا بطرق مختلفة. أسهل لنسخ وضبط ، وهكذا سيكون أكثر وضوحا. سوف أقوم أيضًا بتعديل searchForward لاحقًا ، في مرحلة ما ، ربما في منشور لاحق.
أضفت بعض الأشياء المهمة! لاحظ أن الدوال الثلاث جميعها تقوم الآن بإرجاع نوع! .. هذا هو بناء الجملة الجديد لما كان عليه النوع٪ T (اتحاد الأخطاء). هذا يعني أن الوظيفة يمكنها إرجاع نوع معين أو خطأ ما. عندما أحاول الاتصال بمثل هذه الوظيفة ، يجب علي إما استخدام المحاولة قبل استدعاء الوظيفة ، والتي ترمي الخطأ إلى تكدس المكالمة في حالة حدوث الخطأ أو استخدام catch:
const x = functionCall() catch {}
حيث أتعامل مع الأخطاء في كتلة catch. كما هو مكتوب ، يمكن للقبض ابتلاع أي أخطاء. هذه ممارسة سيئة ، لكن هنا يجعلناها متعرجًا بشكل صريح. إذا واجهت خطأً في كتلة فارغة ، فأنا أذكر إما أنني لا أعتقد أنه يمكن أن يحدث خطأ ، أو لا أحتاج إلى معالجته. في الممارسة العملية ، يمكن أن يكون الأمر مثل TODO ، وفي الواقع من السهل جدًا توضيح ذلك أيضًا!
const x = functionCall() catch { @panic("TODO") }
تذكر أن مثل هذه الحالة لن تحدث أبدًا في كود الإنتاج. أعلم المترجم أنني أعلم ما أقوم به. إذا حدث خطأ ، فسيتعين علي إضافة معالجة الأخطاء.
إذن ما هي الأخطاء التي يجب أن أعود من searchBack أو questForward؟
في askBack:
ptr = sub(u16, ptr, 1) catch return error.OutOfBounds;
لقد استبدلت مؤشر التناقص لاستخدام الوظيفة الفرعية لـ std lib ، والذي يلقي خطأ في تجاوز السعة في حالة حدوث تجاوز سعة. أريد التقاط هذا الخطأ وإرجاع خطأ OutOfBounds بدلاً من ذلك ، والذي أقوم بإنشائه هنا فقط باستخدامه.
الأخطاء Zig هي في الأساس مجموعة من رموز الأخطاء التي يتم إنشاؤها بواسطة برنامج التحويل البرمجي عند استخدامك للخطأ. وهي مضمونة لتكون فريدة من نوعها ويمكن استخدامها كقيم في كتلة التبديل.
أرغب في استخدام OutOfBounds هنا ، لأنه إذا كان مؤشر الذاكرة أقل من الصفر ، فأنا أطلب من وقت التشغيل تجاوز مساحة الذاكرة التي قمت بتخصيصها.
بالمثل في وظيفة searchForward:
if (ptr >= src.len) return error.OutOfBounds;
في هذه الحالة ، إذا كان المؤشر أكبر من src.len ، فإنني ألتقط الخطأ هنا وأرجع الخطأ نفسه.
عند الاتصال:
'[' => if (storage[memptr] == 0) srcptr = try seekForward(src, srcptr), ']' => if (storage[memptr] != 0) srcptr = try seekBack(src, srcptr),
أحاول استدعاء هذه الوظائف. إذا تم استدعاؤها بنجاح ، يتم تنفيذها بشكل صحيح ، وحاول إرجاع srcptr. إذا لم تنجح ، فحاول إنهاء الوظيفة وإرجاع خطأ إلى مكان الاستدعاء إلى الوظيفة بأكملها bf.
قد تكون الدعوة من الرئيسي!
const bf = @import("./bf.zig").bf; // yes, hello const hello_world = "++++++++++[>+++++++>++++++++++>+++>+<<<<-]>++.>+.+++++++..+++.>++.<<+++++++++++++++.>.+++.------.--------.>+.>."; pub fn main() void { storage = []u8{0} ** 30000; bf(hello_world, storage[0..]) catch {}; }
أنا ابتلع هذا الخطأ هنا ، ويجب ألا يتم ذلك ، لكننا سنلاحظ نقطة مهمة حول مدى سهولة تعرج zig عن الأخطاء في مكدس الاتصال. ليس من مسؤولية وظيفة الاستدعاء التحقق من كل حالة خطأ ، ولكن المترجم يفرض استدعاء كل وظيفة يمكن أن تفشل مع المحاولة. يجب أن يتم ذلك دائمًا ، حتى لو تم تجاهل الأخطاء!
يزيل بناء جملة try / catch الجديد العديد من التعاويبات مثل ٪٪ و٪ التي لا يكره الناس كثيرًا.
لقد نفذت الآن 7 من أصل 8 شخصيات من كتابي brainfuck ، وهذا يكفي لتشغيل برنامج "ذي معنى".
برنامج مفيد
هنا البرنامج:
// , const fib = "++++++++++++++++++++++++++++++++++++++++++++>++++++++++++++++++++++++++++++++>++++++++++++++++>>+<<[>>>>++++++++++<<[->+>-[>+>>]>[+[-<+>]>+>>]<<<<<<]>[<+>-]>[-]>>>++++++++++<[->-[>+>>]>[+[-<+>]>+>>]<<<<<]>[-]>>[++++++++++++++++++++++++++++++++++++++++++++++++.[-]]<[++++++++++++++++++++++++++++++++++++++++++++++++.[-]]<<<++++++++++++++++++++++++++++++++++++++++++++++++.[-]<<<<<<<.>.>>[>>+<<-]>[>+<<+>-]>[<+>-]<<<-]<<++...";
لنركض ...
pub fn main() void { storage = []u8{0} ** 30000; bf(fib, storage[0..]) catch {}; }
فويلا!
1, 1, 2, 3, 5, 8, 13, 21, 34, 55, 89, 144, 233, 121, 98, 219,
تعود لي ذاكرة واحدة في كل مرة أفكر في سلسلة Fibonacci ... اكتشفت ذلك من برنامج PBS (خدمة البث العامة ، وهو برنامج بث تلفزيوني أمريكي غير تجاري) في الثمانينات ، وأتذكر ذلك دائمًا. اعتقدت أنه سيُنسى ، لكن يوتيوب شيء رائع .
كيف يمكنني تحسين هذا؟
لقد أشرت بالفعل في عدد قليل من TODOs. لا ينبغي لي أن استخدمت stderr للإخراج. أريد استخدام stdout.
في كل مرة أفتح فيها المترجم الفوري ، أفتح الدفق في stdout وأطبع فيه:
const io = std.io; ... pub fn bf(src: []const u8, storage: []u8) !void { const stdout = &(io.FileOutStream.init(&(io.getStdOut() catch unreachable)).stream); ... '.' => stdout.print("{c}", storage[memptr]) catch unreachable, ...
ما الذي يحدث هنا؟
أدعو io.getStdOut () ، والذي يمكن أن يولد أخطاء (ومرة أخرى ، أبتلع بوضوح خطأ محتمل في حالة عدم إمكانية الوصول إلى catch - إذا أرجعت هذه الوظيفة خطأً ، فسوف يتعطل البرنامج!). أقوم بتهيئة الدفق وأخذ مؤشرًا إليه وأعده كدفق إخراج يمكنني الكتابة إليه من خلال الاتصال بالطباعة. تقبل الطباعة سلسلة منسقة ، تمامًا كما تفعل warn ، لذا فإن الاستبدال يكون مباشرًا. طباعة يمكن أن تولد أيضا خطأ ، وأنا ابتلع هذه الأخطاء أيضا.في برنامج مكتوب بشكل صحيح ، يجب أن تأخذ في الاعتبار الأخطاء المحتملة لفتح stdout ، وكذلك الأخطاء المحتملة لمحاولات الكتابة إلى stdout. يجعل Zig من السهل جدًا تجاهل هذه الأخطاء طالما تعلم أنك تتجاهلها.ماذا يحدث إذا قررت أنني أريد تحويل النموذج الأولي الخاص بي إلى إصدار؟ هل سأجلس مع فنجان من القهوة وأقوم بعمل شاق من معالجة الأخطاء ، والاعتماد على عقود من الخبرة والمعرفة لسرد كل حالة خطأ ممكن ، وكيف يمكنني التعامل معها؟ ولكن ماذا لو لم يكن لدي عقود من الخبرة والمعرفة؟ كل شيء على مايرام ، وسوف تفعل ذلك متعرج!أريد أن أثبت وجود شيء قوي ، خطأ في الإخراج! const bf = @import("./bf.zig").bf; const warn = @import("std").debug.warn; const serpinsky = "++++++++[>+>++++<<-]>++>>+<[-[>>+<<-]+>>]>+[ -<<<[ ->[+[-]+>++>>>-<<]<[<]>>++++++[<<+++++>>-]+<<++.[-]<< ]>.>+[>>]>+ ] "; pub fn main() void { var storage = []u8{0} ** 30000; bf(serpinsky, storage[0..]) catch unreachable; }
أعلم أن فرنك بلجيكي يمكن أن يولد أخطاء لأنه يعود! أنا ابتلع هذا الخطأ في جانب الاتصال ، في الوظيفة الرئيسية. عندما أكون مستعدًا لقبول قدري وفعل الشيء الصحيح ، يمكنني أن ألاحظ الأخطاء المحتملة مثل هذا: const bf = @import("./bf.zig").bf; const warn = @import("std").debug.warn; const serpinsky = "++++++++[>+>++++<<-]>++>>+<[-[>>+<<-]+>>]>+[ -<<<[ ->[+[-]+>++>>>-<<]<[<]>>++++++[<<+++++>>-]+<<++.[-]<< ]>.>+[>>]>+ ] "; pub fn main() void { var storage = []u8{0} ** 30000; bf(serpinsky, storage[0..]) catch |err| switch (err) { }; }
المترجم هو الآن صديقي! /Users/jfo/code/zigfuck/main.zig:7:46: error: error.OutOfBounds not handled in switch shell returned 1
يجب أن يكون هذا الخطأ مألوفًا لك ، نظرًا لأنه تم إنشاؤه من الوظائف الإضافية للفرنك بلجيكي والوظائف الإضافية ولكن دعونا نتخيل أنني أنظر إلى الأخطاء الناتجة عن stdout التي ابتلعتها في bf. بدلاً من ابتلاعها ، يجب أن أقوم بدفعها إلى أعلى باستخدام السلسلة. تذكر أنه باستخدام استدعاء للدالة التي تولد أخطاء دون توقف ، فإننا نستخدم try ، التي تنهي المهمة عند حدوث خطأ ، ونزود وظيفة الاتصال بمعالجة أي أخطاء محتملة.لذلك ، بدلا من: const io = std.io; ... pub fn bf(src: []const u8, storage: []u8) !void { const stdout = &(io.FileOutStream.init(&(io.getStdOut() catch unreachable)).stream); ... '.' => stdout.print("{c}", storage[memptr]) catch unreachable, ...
نحن نفعل: const io = std.io; ... pub fn bf(src: []const u8, storage: []u8) !void { const stdout = &(io.FileOutStream.init(&(try io.getStdOut())).stream); ... '.' => try stdout.print("{c}", storage[memptr]), ...
نحن نجمع: const bf = @import("./bf.zig").bf; const warn = @import("std").debug.warn; const serpinsky = "++++++++[>+>++++<<-]>++>>+<[-[>>+<<-]+>>]>+[ -<<<[ ->[+[-]+>++>>>-<<]<[<]>>++++++[<<+++++>>-]+<<++.[-]<< ]>.>+[>>]>+ ] "; pub fn main() void { var storage = []u8{0} ** 30000; bf(serpinsky, storage[0..]) catch |err| switch (err) { }; }
والحصول على قائمة بجميع الأخطاء المحتملة التي يمكنني الحصول عليها عن طريق استدعاء الوظيفة! /Users/jfo/code/zigfuck/main.zig:7:46: error: error.SystemResources not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.OperationAborted not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.IoPending not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.BrokenPipe not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.Unexpected not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.WouldBlock not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.FileClosed not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.DestinationAddressRequired not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.DiskQuota not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.FileTooBig not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.InputOutput not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.NoSpaceLeft not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.AccessDenied not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.OutOfBounds not handled in switch /Users/jfo/code/zigfuck/main.zig:7:46: error: error.NoStdHandles not handled in switch shell returned 1
يعطيني Zig الفرصة للتعامل مع هذه الأخطاء بعناية إذا أردت ذلك أو أستطيع القيام به! أقوم بإجراء التبديل استنادًا إلى قيم الخطأ ، والتعامل مع الحالات إذا أردت ، وتخطيها إذا أردت تخطيها. pub fn main() void { var storage = []u8{0} ** 30000; bf(serpinsky, storage[0..]) catch |err| switch (err) { error.OutOfBounds => @panic("Out Of Bounds!"), else => @panic("IO error") }; }
لا يزال هذا غير صحيح عند معالجة الأخطاء ، بالمعنى الدقيق للكلمة ، لكني أريد فقط أن أوضح مدى ذكاء Zig من خلال الإبلاغ عن جميع أنواع حالات الخطأ إلى وظيفة الاتصال! وعندما يحدث خطأ ، تحصل على تتبع خطأ بدلاً من تتبع مكدس! شيء رائع!تودو
هناك العديد من التحسينات المختلفة التي يمكنك إجراؤها باستخدام المترجم الفوري! تحتاج حقًا إلى التعامل مع جميع الأخطاء بشكل صحيح ، ومن الواضح أنك تحتاج إلى تنفيذ عامل التشغيل "،" الذي يعمل في brainfuck كدالة getc ، مما يسمح لك بإدخال البيانات في البرنامج عند تنفيذه. تحتاج أيضًا إلى تمكين قراءة الملف المصدر في المخزن المؤقت وتفسيره بدلاً من استخدام التعليمات البرمجية المصدر bf الثابت الترميز. هناك أيضًا بعض التحسينات التي ليست ضرورية تمامًا ، ولكنها قد توضح بعض ميزات Zig. بدلاً من إلقاءها جميعًا في نهاية المنشور ، سأقوم بتقسيمها إلى أجزاء ونشرها في مشاركات مستقبلية ، والتي ستكون أصغر وأسهل في الهضم.الخاتمة
آمل أن يمنحك هذا المشروع المصغّر نصف النهائي بعض التفاصيل حول شكل Zig code وما يمكن استخدامه. Zig ليس سكينًا سويسريًا ، إنه ليس أداة مثالية لكل شيء ، إنه يركز على أشياء معينة ، على أن يكون لغة نظام واقعية يمكن استخدامها معًا أو بدلاً من C و C ++. هذا جعلني أتناول بعناية استخدام الذاكرة وإدارة الذاكرة ومعالجة الأخطاء. في بيئة ذات موارد محدودة ، هذه ميزة مفيدة وليست خطأ. إن Zig حتمية ، وليس لديها أي غموض ، ويحاول تسهيل كتابة التعليمات البرمجية الموثوقة في بيئة يصعب القيام بها تقليديًا.لقد وصفت جزءًا صغيرًا فقط من بناء الجملة وميزات Zig ، فهناك العديد من التغييرات المثيرة للاهتمام التي وصلت إلى اللغة في الإصدار 0.2.0 أو أعلى! يتم تجميع كل التعليمات البرمجية التي كتبت في وضع تصحيح الأخطاء ، وهو الأمثل لفحوصات الأمان ولتقليل وقت التحويل البرمجي لجعل التكرار أسرع! هناك أوضاع سريعة - وآمنة - ، وسوف يكون هناك المزيد في المستقبل . يمكنك قراءة المزيد حول الاختلافات والتفسيرات حول هذه الأوضاع هنا .أنا مندهش باستمرار من سرعة واتجاه التنمية متعرج. لا يزال هناك الكثير في الطريق ، وسيظل كذلك حتى إصدار الإصدار 1.0.0 ، وإذا قررت تجربة Zig ، فقط تذكر أنه توجد الكثير من الأفكار الجيدة ، وأتطلع إلى تنفيذها!جربه وانضم إلى #zig في freenode في أي وقت إذا كانت لديك أسئلة.