جو ستورك ، الجزء 4: تنفيذ العبارات واختتامها
نشرت: 2022-03-11في سعينا لإنشاء لغة برمجة خفيفة الوزن باستخدام C ++ ، بدأنا بإنشاء الرمز المميز الخاص بنا قبل ثلاثة أسابيع ، ثم قمنا بتنفيذ تقييم التعبير في الأسبوعين التاليين.
الآن ، حان الوقت للختام وتقديم لغة برمجة كاملة لن تكون قوية مثل لغة البرمجة الناضجة ولكنها ستحتوي على جميع الميزات الضرورية ، بما في ذلك مساحة صغيرة جدًا.
أجد أنه من المضحك كيف تمتلك الشركات الجديدة أقسامًا للأسئلة الشائعة على مواقعها على الويب لا تجيب على الأسئلة التي يتم طرحها بشكل متكرر ولكن الأسئلة التي يريدون طرحها. سأفعل نفس الشيء هنا. كثيرًا ما يسألني الأشخاص الذين يتابعون عملي عن سبب عدم ترجمة Stork لبعض الرموز الثانوية أو على الأقل إلى لغة وسيطة.
لماذا لا يترجم ستورك إلى Bytecode؟
أنا سعيد للإجابة على هذا السؤال. كان هدفي هو تطوير لغة برمجة نصية صغيرة الحجم تتكامل بسهولة مع C ++. ليس لدي تعريف صارم "للحجم الصغير" ، لكني أتخيل مترجمًا سيكون صغيرًا بما يكفي لتمكين إمكانية النقل إلى الأجهزة الأقل قوة ولن يستهلك الكثير من الذاكرة عند تشغيله.
لم أركز على السرعة ، حيث أعتقد أنك ستعمل على البرمجة بلغة C ++ إذا كانت لديك مهمة حرجة بالنسبة للوقت ، ولكن إذا كنت بحاجة إلى نوع من التمدد ، فقد تكون لغة مثل Stork مفيدة.
لا أدعي أنه لا توجد لغات أخرى أفضل يمكنها إنجاز مهمة مماثلة (على سبيل المثال ، Lua). سيكون الأمر مأساويًا حقًا إذا لم تكن موجودة ، وأنا فقط أعطيك فكرة عن حالة استخدام هذه اللغة.
نظرًا لأنه سيتم تضمينه في C ++ ، أجد أنه من السهل استخدام بعض الميزات الحالية لـ C ++ بدلاً من كتابة نظام بيئي كامل يخدم غرضًا مشابهًا. ليس هذا فقط ، ولكني أجد هذا النهج أكثر إثارة للاهتمام أيضًا.
كما هو الحال دائمًا ، يمكنك العثور على شفرة المصدر الكاملة على صفحة GitHub الخاصة بي. الآن ، دعنا نلقي نظرة فاحصة على تقدمنا.
التغييرات
حتى هذا الجزء ، كان Stork منتجًا كاملاً جزئيًا ، لذلك لم أتمكن من رؤية جميع عيوبه وعيوبه. ومع ذلك ، نظرًا لأنه اتخذ شكلاً أكثر اكتمالاً ، فقد قمت بتغيير الأشياء التالية التي تم تقديمها في الأجزاء السابقة:
- الوظائف لم تعد متغيرات بعد الآن. هناك
function_lookup
منفصلة فيcompiler_context
الآن. تمت إعادة تسميةfunction_param_lookup
إلىparam_lookup
لتجنب الالتباس. - لقد غيرت طريقة استدعاء الوظائف. هناك طريقة
call
فيruntime_context
تأخذstd::vector
من الوسيطات ، وتخزن مؤشر قيمة الإرجاع القديم ، وتدفع الوسيطات إلى المكدس ، وتغير مؤشر قيمة الإرجاع ، وتستدعي الوظيفة ، وتنبثق الوسائط من المكدس ، وتستعيد مؤشر قيمة الإرجاع القديم ، و إرجاع النتيجة. بهذه الطريقة ، لا يتعين علينا الاحتفاظ بمكدس مؤشرات القيمة المرتجعة ، كما كان من قبل ، لأن مكدس C ++ يخدم هذا الغرض. - تمت إضافة فئات RAII في
compiler_context
التي يتم إرجاعها من خلال الاستدعاءات إلىscope
وظائف الأعضاءfunction
. يقوم كل من هذه الكائنات بإنشاءlocal_identifier_lookup
وparam_identifier_lookup
، على التوالي ، في منشئيهم واستعادة الحالة القديمة في التدمير. - تمت إضافة فئة RAII في
runtime_context
، تم إرجاعها بواسطة دالة العضوget_scope
. تخزن هذه الوظيفة حجم المكدس في المنشئ الخاص بها وتستعيده في المدمر الخاص به. - لقد أزلت الكلمة الأساسية
const
والأشياء الثابتة بشكل عام. يمكن أن تكون مفيدة ولكنها ليست ضرورية للغاية. - تمت إزالة كلمة
var
، حيث إنها ليست ضرورية على الإطلاق في الوقت الحالي. - لقد أضفت
sizeof
الكلمات الرئيسية ، والتي ستتحقق من حجم المصفوفة في وقت التشغيل. ربما يجد بعض مبرمجي C ++ اختيار الاسمsizeof
، حيث يتم تشغيل حجم C ++ في وقت الترجمة ، لكنني اخترت هذه الكلمة الأساسية لتجنب الاصطدام ببعض اسم المتغير الشائع - على سبيل المثال ،size
. - أضفت
tostring
keyword ، والتي تحول صراحةً أي شيء إلىstring
. لا يمكن أن تكون وظيفة ، لأننا لا نسمح بزيادة التحميل على الوظيفة. - تغييرات مختلفة أقل إثارة للاهتمام.
بناء الجملة
نظرًا لأننا نستخدم بناء جملة مشابه جدًا للغة C ولغات البرمجة المرتبطة بها ، فسأقدم لك التفاصيل التي قد لا تكون واضحة.
إقرارات النوع المتغير هي كما يلي:
-
void
، يُستخدم فقط لنوع إرجاع الوظيفة -
number
-
string
-
T[]
هي مصفوفة لما يحمل عناصر من النوعT
-
R(P1,...,Pn)
هي دالة تقوم بإرجاع النوعR
وتتلقى وسيطات من الأنواعP1
إلىPn
. يمكن أن يبدأ كل نوع من هذه الأنواع بـ&
إذا تم تمريره بالمرجع.
إعلان الوظيفة هو كما يلي: [public] function R name(P1 p1, … Pn pn)
لذلك ، يجب أن تكون مسبوقة function
. إذا كانت مسبوقة بـ public
، فيمكن استدعاؤها من C ++. إذا لم تُرجع الدالة القيمة ، فسيتم تقييمها إلى القيمة الافتراضية لنوع الإرجاع الخاص بها.
نسمح for
-loop مع تصريح في التعبير الأول. نسمح أيضًا بعبارة if
-statement و switch
-stat مع تعبير تهيئة ، كما هو الحال في C ++ 17. تبدأ عبارة if
-block بـ if
-block ، متبوعة بصفر أو أكثر من كتل elif
، واختياريًا ، كتلة else
-block. إذا تم الإعلان عن المتغير في تعبير التهيئة الخاص بعبارة if
، فسيكون مرئيًا في كل من هذه الكتل.
نسمح برقم اختياري بعد تعليمة break
يمكن أن ينفصل عن عدة حلقات متداخلة. لذلك يمكنك الحصول على الكود التالي:
for (number i = 0; i < 100; ++i) { for(number j = 0; j < 100; ++j) { if (rnd(100) == 0) { break 2; } } }
أيضا ، سوف ينفصل عن كلتا الحلقتين. يتم التحقق من صحة هذا الرقم في وقت الترجمة. كم ذلك رائع؟
مترجم
تمت إضافة العديد من الميزات في هذا الجزء ، ولكن إذا حصلت على تفاصيل أكثر من اللازم ، فمن المحتمل أن أفقد حتى القراء الأكثر إصرارًا الذين لا يزالون يتحملون معي. لذلك ، سأتخطى عن قصد جزءًا كبيرًا جدًا من القصة - التجميع.
هذا لأنني وصفته بالفعل في الجزأين الأول والثاني من سلسلة المدونات هذه. كنت أركز على التعبيرات ، لكن تجميع أي شيء آخر لا يختلف كثيرًا.
ومع ذلك ، سأعطيكم مثالاً واحداً. هذا الرمز يجمع while
البيانات:
statement_ptr compile_while_statement( compiler_context& ctx, tokens_iterator& it, possible_flow pf ) { parse_token_value(ctx, it, reserved_token::kw_while); parse_token_value(ctx, it, reserved_token::open_round); expression<number>::ptr expr = build_number_expression(ctx, it); parse_token_value(ctx, it, reserved_token::close_round); block_statement_ptr block = compile_block_statement(ctx, it, pf); return create_while_statement(std::move(expr), std::move(block)); }
كما ترى ، فهي بعيدة كل البعد عن التعقيد. يوزع while
، ثم (
، ثم يبني تعبيرًا رقميًا (ليس لدينا قيم منطقية) ، ثم يوزع )
.
بعد ذلك ، يقوم بتجميع بيان كتلة قد يكون في الداخل {
و }
أو لا (نعم ، لقد سمحت بكتل ذات تعليمة واحدة) وينشئ تعليمة while
في النهاية.
أنت بالفعل على دراية بأول وسيطتي وظيفتين. الثالث ، الذي possible_flow
، يُظهر أوامر تغيير التدفق المسموح بها ( continue
، break
، return
) في السياق الذي نقوم بتحليله. يمكنني الاحتفاظ بهذه المعلومات في الكائن إذا كانت عبارات التجميع عبارة عن وظائف عضو في بعض فئات compiler
، لكنني لست معجبًا كبيرًا بفئات الماموث ، وسيكون المترجم بالتأكيد أحد هذه الفئات. تمرير حجة إضافية ، خاصةً ضعيفة ، لن يؤذي أي شخص ، ومن يدري ، ربما في يوم من الأيام سنكون قادرين على موازنة الشفرة.
هناك جانب آخر مثير للاهتمام في التجميع أود أن أوضحه هنا.
إذا أردنا دعم سيناريو حيث تستدعي وظيفتان بعضهما البعض ، فيمكننا القيام بذلك بالطريقة C: من خلال السماح بالتصريح الأمامي أو وجود مرحلتين من مراحل التجميع.
اخترت النهج الثاني. عندما يتم العثور على تعريف الوظيفة ، سنقوم بتحليل نوعها واسمها في الكائن المسمى incomplete_function
. بعد ذلك ، سوف نتخطى جسمه ، بدون تفسير ، ببساطة عن طريق حساب مستوى تداخل الأقواس المتعرجة حتى نغلق الدعامة الأولى المتعرجة. سنجمع الرموز المميزة في العملية ، ونبقيها في حالة incomplete_function
، ونضيف معرف وظيفة إلى compiler_context
.
بمجرد تمرير الملف بأكمله ، سنقوم بتجميع كل وظيفة بشكل كامل ، بحيث يمكن استدعاؤها في وقت التشغيل. بهذه الطريقة ، يمكن لكل دالة استدعاء أي وظيفة أخرى في الملف والوصول إلى أي متغير عام.
يمكن تهيئة المتغيرات العامة من خلال استدعاءات لنفس الوظائف ، مما يقودنا على الفور إلى مشكلة "الدجاجة والبيضة" القديمة بمجرد وصول هذه الوظائف إلى المتغيرات غير المهيأة.
في حالة حدوث ذلك ، يتم حل المشكلة عن طريق طرح runtime_exception
وقت التشغيل - وهذا فقط لأنني لطيف. فرانكي ، انتهاك الوصول هو أقل ما يمكنك الحصول عليه كعقوبة لكتابة مثل هذا الرمز.
النطاق العالمي
هناك نوعان من الكيانات التي يمكن أن تظهر في النطاق العام:
- المتغيرات العالمية
- المهام
يمكن تهيئة كل متغير عام بتعبير يُرجع النوع الصحيح. يتم تكوين المُهيئ لكل متغير عام.
يقوم كل مُهيئ بإرجاع lvalue
، لذا فهم يعملون كمنشئين للمتغيرات العالمية. عندما لا يتم توفير تعبير لمتغير عام ، يتم إنشاء المُهيئ الافتراضي.
هذه هي وظيفة العضو initialize
في runtime_context
:
void runtime_context::initialize() { _globals.clear(); for (const auto& initializer : _initializers) { _globals.emplace_back(initializer->evaluate(*this)); } }
يطلق عليه من المنشئ. يقوم بمسح حاوية المتغير العام ، كما يمكن تسميتها صراحة ، لإعادة تعيين حالة runtime_context
.
كما ذكرت سابقًا ، نحتاج إلى التحقق مما إذا كنا نصل إلى متغير عالمي غير مهيأ. لذلك ، هذا هو الموصل المتغير العام:
variable_ptr& runtime_context::global(int idx) { runtime_assertion( idx < _globals.size(), "Uninitialized global variable access" ); return _globals[idx]; }
إذا تم تقييم الوسيطة الأولى إلى false
، runtime_assertion
يرمي runtime_error
مع الرسالة المقابلة.
يتم تنفيذ كل دالة على أنها lambda التي تلتقط العبارة الفردية ، والتي يتم تقييمها بعد ذلك باستخدام runtime_context
الذي تتلقاه الوظيفة.
نطاق الوظيفة
كما ترون من تجميع while
-statement ، يتم استدعاء المترجم بشكل متكرر ، بدءًا من تعليمة block ، والتي تمثل كتلة الوظيفة بأكملها.
هذه هي الفئة الأساسية المجردة لجميع العبارات:
class statement { statement(const statement&) = delete; void operator=(const statement&) = delete; protected: statement() = default; public: virtual flow execute(runtime_context& context) = 0; virtual ~statement() = default; };
الوظيفة الوحيدة بصرف النظر عن الوظائف الافتراضية هي execute
، والتي تقوم بتنفيذ منطق العبارة على runtime_context
وإرجاع flow
، والذي يحدد المكان الذي سينتقل إليه منطق البرنامج بعد ذلك.
enum struct flow_type{ f_normal, f_break, f_continue, f_return, }; class flow { private: flow_type _type; int _break_level; flow(flow_type type, int break_level); public: flow_type type() const; int break_level() const; static flow normal_flow(); static flow break_flow(int break_level); static flow continue_flow(); static flow return_flow(); flow consume_break(); };
وظائف المنشئ الثابتة تشرح نفسها بنفسها ، وقد قمت بكتابتها لمنع flow
غير المنطقي break_level
غير الصفري والنوع المختلف عن flow_type::f_break
.
الآن ، consume_break
تدفق كسر بمستوى كسر واحد أقل أو ، إذا وصل مستوى الكسر إلى الصفر ، التدفق الطبيعي.
الآن ، سوف نتحقق من جميع أنواع البيانات:
class simple_statement: public statement { private: expression<void>::ptr _expr; public: simple_statement(expression<void>::ptr expr): _expr(std::move(expr)) { } flow execute(runtime_context& context) override { _expr->evaluate(context); return flow::normal_flow(); } };
هنا ، simple_statement
هو البيان الذي يتم إنشاؤه من تعبير. يمكن ترجمة كل تعبير كتعبير يُرجع void
، simple_statement
يمكن إنشاء عبارة بسيطة منه. نظرًا لأنه continue
يمكن أن يكون break
أو المتابعة أو return
جزءًا من تعبير ، فإن simple_statement
يُرجع flow::normal_flow()
.
class block_statement: public statement { private: std::vector<statement_ptr> _statements; public: block_statement(std::vector<statement_ptr> statements): _statements(std::move(statements)) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const statement_ptr& statement : _statements) { if ( flow f = statement->execute(context); f.type() != flow_type::f_normal ){ return f; } } return flow::normal_flow(); } };
تحافظ block_statement
على std::vector
للبيانات. يقوم بتنفيذها واحدا تلو الآخر. إذا قام كل واحد منهم بإرجاع تدفق غير عادي ، فإنه يعيد هذا التدفق على الفور. يستخدم كائن نطاق RAII للسماح بإعلانات متغير النطاق المحلي.
class local_declaration_statement: public statement { private: std::vector<expression<lvalue>::ptr> _decls; public: local_declaration_statement(std::vector<expression<lvalue>::ptr> decls): _decls(std::move(decls)) { } flow execute(runtime_context& context) override { for (const expression<lvalue>::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return flow::normal_flow(); } };
يقيّم local_declaration_statement
class break_statement: public statement { private: int _break_level; public: break_statement(int break_level): _break_level(break_level) { } flow execute(runtime_context&) override { return flow::break_flow(_break_level); } };
break_statement
له مستوى الفاصل الذي تم تقييمه في وقت الترجمة. يقوم فقط بإرجاع التدفق الذي يتوافق مع مستوى الاختراق هذا.
class continue_statement: public statement { public: continue_statement() = default; flow execute(runtime_context&) override { return flow::continue_flow(); } };
ترجع الدالة continue_statement
فقط flow::continue_flow()
.
class return_statement: public statement { private: expression<lvalue>::ptr _expr; public: return_statement(expression<lvalue>::ptr expr) : _expr(std::move(expr)) { } flow execute(runtime_context& context) override { context.retval() = _expr->evaluate(context); return flow::return_flow(); } }; class return_void_statement: public statement { public: return_void_statement() = default; flow execute(runtime_context&) override { return flow::return_flow(); } };
return_statement
و return_void_statement
كلاً من flow::return_flow()
. الاختلاف الوحيد هو أن الأول يحتوي على التعبير الذي يقيّمه إلى القيمة المرجعة قبل أن يعود.

class if_statement: public statement { private: std::vector<expression<number>::ptr> _exprs; std::vector<statement_ptr> _statements; public: if_statement( std::vector<expression<number>::ptr> exprs, std::vector<statement_ptr> statements ): _exprs(std::move(exprs)), _statements(std::move(statements)) { } flow execute(runtime_context& context) override { for (size_t i = 0; i < _exprs.size(); ++i) { if (_exprs[i]->evaluate(context)) { return _statements[i]->execute(context); } } return _statements.back()->execute(context); } }; class if_declare_statement: public if_statement { private: std::vector<expression<lvalue>::ptr> _decls; public: if_declare_statement( std::vector<expression<lvalue>::ptr> decls, std::vector<expression<number>::ptr> exprs, std::vector<statement_ptr> statements ): if_statement(std::move(exprs), std::move(statements)), _decls(std::move(decls)) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const expression<lvalue>::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return if_statement::execute(context); } };
if_statement
، والتي تم إنشاؤها لكتلة if
-block ، وصفر أو أكثر من كتل elif
، وكتلة else
-block (والتي يمكن أن تكون فارغة) ، كل تعبير من تعبيراتها حتى يتم تقييم تعبير واحد إلى 1
. ثم ينفذ هذا الحظر ويعيد نتيجة التنفيذ. إذا لم يتم تقييم أي تعبير إلى 1
، فسيعيد تنفيذ آخر كتلة ( else
).
if_declare_statement
هي البيان الذي يحتوي على إعلانات كجزء أول من جملة if. يدفع جميع المتغيرات المعلنة إلى المكدس ثم ينفذ صنفه الأساسي ( if_statement
).
class switch_statement: public statement { private: expression<number>::ptr _expr; std::vector<statement_ptr> _statements; std::unordered_map<number, size_t> _cases; size_t _dflt; public: switch_statement( expression<number>::ptr expr, std::vector<statement_ptr> statements, std::unordered_map<number, size_t> cases, size_t dflt ): _expr(std::move(expr)), _statements(std::move(statements)), _cases(std::move(cases)), _dflt(dflt) { } flow execute(runtime_context& context) override { auto it = _cases.find(_expr->evaluate(context)); for ( size_t idx = (it == _cases.end() ? _dflt : it->second); idx < _statements.size(); ++idx ) { switch (flow f = _statements[idx]->execute(context); f.type()) { case flow_type::f_normal: break; case flow_type::f_break: return f.consume_break(); default: return f; } } return flow::normal_flow(); } }; class switch_declare_statement: public switch_statement { private: std::vector<expression<lvalue>::ptr> _decls; public: switch_declare_statement( std::vector<expression<lvalue>::ptr> decls, expression<number>::ptr expr, std::vector<statement_ptr> statements, std::unordered_map<number, size_t> cases, size_t dflt ): _decls(std::move(decls)), switch_statement(std::move(expr), std::move(statements), std::move(cases), dflt) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const expression<lvalue>::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return switch_statement::execute(context); } };
ينفذ switch_statement
واحدًا تلو الآخر ، لكنه يقفز أولاً إلى الفهرس المناسب الذي يحصل عليه من تقييم التعبير. إذا أرجع أي من عباراته تدفقًا غير عادي ، فسيعيد هذا التدفق على الفور. إذا كان يحتوي على flow_type::f_break
، فسيستهلك فاصلًا واحدًا أولاً.
يسمح switch_declare_statement
بالإعلان في رأسه. لا يسمح أي من هؤلاء بالإعلان في الجسد.
class while_statement: public statement { private: expression<number>::ptr _expr; statement_ptr _statement; public: while_statement(expression<number>::ptr expr, statement_ptr statement): _expr(std::move(expr)), _statement(std::move(statement)) { } flow execute(runtime_context& context) override { while (_expr->evaluate(context)) { switch (flow f = _statement->execute(context); f.type()) { case flow_type::f_normal: case flow_type::f_continue: break; case flow_type::f_break: return f.consume_break(); case flow_type::f_return: return f; } } return flow::normal_flow(); } };
class do_statement: public statement { private: expression<number>::ptr _expr; statement_ptr _statement; public: do_statement(expression<number>::ptr expr, statement_ptr statement): _expr(std::move(expr)), _statement(std::move(statement)) { } flow execute(runtime_context& context) override { do { switch (flow f = _statement->execute(context); f.type()) { case flow_type::f_normal: case flow_type::f_continue: break; case flow_type::f_break: return f.consume_break(); case flow_type::f_return: return f; } } while (_expr->evaluate(context)); return flow::normal_flow(); } };
while_statement
1
do_while_statement
إذا أرجع التنفيذ flow_type::f_break
، فسيستهلكونه ويعيدونه. إذا قام بإرجاع flow_type::f_return
، فإنه يعيده. في حالة التنفيذ العادي ، أو الاستمرار ، لا يفعلون شيئًا.
قد يبدو كما لو أن continue
ليس لها تأثير. ومع ذلك ، تأثر البيان الداخلي به. إذا كانت ، على سبيل المثال ، block_statement
، فلن يتم تقييمها حتى النهاية.
أجد أنه من الرائع أن while_statement
يتم تنفيذها باستخدام C ++ while
، و do-statement
مع C ++ do-while
.
class for_statement_base: public statement { private: expression<number>::ptr _expr2; expression<void>::ptr _expr3; statement_ptr _statement; public: for_statement_base( expression<number>::ptr expr2, expression<void>::ptr expr3, statement_ptr statement ): _expr2(std::move(expr2)), _expr3(std::move(expr3)), _statement(std::move(statement)) { } flow execute(runtime_context& context) override { for (; _expr2->evaluate(context); _expr3->evaluate(context)) { switch (flow f = _statement->execute(context); f.type()) { case flow_type::f_normal: case flow_type::f_continue: break; case flow_type::f_break: return f.consume_break(); case flow_type::f_return: return f; } } return flow::normal_flow(); } }; class for_statement: public for_statement_base { private: expression<void>::ptr _expr1; public: for_statement( expression<void>::ptr expr1, expression<number>::ptr expr2, expression<void>::ptr expr3, statement_ptr statement ): for_statement_base( std::move(expr2), std::move(expr3), std::move(statement) ), _expr1(std::move(expr1)) { } flow execute(runtime_context& context) override { _expr1->evaluate(context); return for_statement_base::execute(context); } }; class for_declare_statement: public for_statement_base { private: std::vector<expression<lvalue>::ptr> _decls; expression<number>::ptr _expr2; expression<void>::ptr _expr3; statement_ptr _statement; public: for_declare_statement( std::vector<expression<lvalue>::ptr> decls, expression<number>::ptr expr2, expression<void>::ptr expr3, statement_ptr statement ): for_statement_base( std::move(expr2), std::move(expr3), std::move(statement) ), _decls(std::move(decls)) { } flow execute(runtime_context& context) override { auto _ = context.enter_scope(); for (const expression<lvalue>::ptr& decl : _decls) { context.push(decl->evaluate(context)); } return for_statement_base::execute(context); } };
يتم تنفيذ for_statement
و for_statement_declare
بشكل مشابه مثل while_statement
و do_statement
. إنها موروثة من فئة for_statement_base
، والتي تقوم بمعظم المنطق. يتم إنشاء for_statement_declare
عندما يكون الجزء الأول من for
-loop هو إعلان متغير.

هذه كلها فئات بيان لدينا. إنها اللبنات الأساسية لوظائفنا. عندما يتم إنشاء runtime_context
، فإنه يحتفظ بهذه الوظائف. إذا تم الإعلان عن الوظيفة بالكلمة الأساسية public
، فيمكن استدعاؤها بالاسم.
هذا يخلص إلى الوظيفة الأساسية لـ Stork. كل شيء آخر سأصفه هو أفكار لاحقة أضفتها لجعل لغتنا أكثر فائدة.
مجموعات
المصفوفات عبارة عن حاويات متجانسة ، حيث يمكن أن تحتوي على عناصر من نوع واحد فقط. إذا أردنا حاويات غير متجانسة ، تتبادر الهياكل على الفور إلى الذهن.
ومع ذلك ، هناك حاويات غير متجانسة تافهة: tuples. يمكن لـ Tuples الاحتفاظ بالعناصر من أنواع مختلفة ، ولكن يجب معرفة أنواعها في وقت الترجمة. هذا مثال على إعلان tuple في Stork:
[number, string] t = {22321, "Siveric"};
هذا يعلن عن زوج من number
string
ويهيئها.
يمكن استخدام قوائم التهيئة لتهيئة المصفوفات أيضًا. عندما لا تتطابق أنواع التعبيرات في قائمة التهيئة مع نوع المتغير ، سيحدث خطأ في المترجم.
نظرًا لأن المصفوفات يتم تنفيذها كحاويات لـ variable_ptr
، فقد حصلنا على تنفيذ وقت تشغيل tuples مجانًا. إنه وقت التجميع عندما نؤكد النوع الصحيح من المتغيرات المضمنة.
الوحدات
سيكون من الجيد إخفاء تفاصيل التنفيذ عن مستخدم Stork وتقديم اللغة بطريقة أكثر سهولة في الاستخدام.
هذا هو الفصل الذي سيساعدنا في تحقيق ذلك. أقدمها بدون تفاصيل التنفيذ:
class module { ... public: template<typename R, typename... Args> void add_external_function(const char* name, std::function<R(Args...)> f); template<typename R, typename... Args> auto create_public_function_caller(std::string name); void load(const char* path); bool try_load(const char* path, std::ostream* err = nullptr) noexcept; void reset_globals(); ... };
سيتم load
الدوال و try_load
وتحميل نص Stork النصي من المسار المحدد. أولاً ، يمكن لأحدهم إلقاء stork::error
، ولكن الثاني سيقبض عليه ويطبعه على الإخراج ، إذا تم توفيره.
ستعمل وظيفة reset_globals
على إعادة تهيئة المتغيرات العامة.
يجب استدعاء الدالتين add_external_functions
و create_public_function_caller
قبل التجميع. الأول يضيف دالة C ++ يمكن استدعاؤها من Stork. الثاني ينشئ الكائن القابل للاستدعاء الذي يمكن استخدامه لاستدعاء وظيفة Stork من C ++. سيؤدي ذلك إلى حدوث خطأ في وقت الترجمة إذا لم يتطابق نوع الوظيفة العامة مع R(Args…)
أثناء تجميع نص Stork النصي.
لقد أضفت العديد من الوظائف القياسية التي يمكن إضافتها إلى وحدة Stork.
void add_math_functions(module& m); void add_string_functions(module& m); void add_trace_functions(module& m); void add_standard_functions(module& m);
مثال
فيما يلي مثال على أحد نصوص ستورك:
function void swap(number& x, number& y) { number tmp = x; x = y; y = tmp; } function void quicksort( number[]& arr, number begin, number end, number(number, number) comp ) { if (end - begin < 2) return; number pivot = arr[end-1]; number i = begin; for (number j = begin; j < end-1; ++j) if (comp(arr[j], pivot)) swap(&arr[i++], &arr[j]); swap (&arr[i], &arr[end-1]); quicksort(&arr, begin, i, comp); quicksort(&arr, i+1, end, comp); } function void sort(number[]& arr, number(number, number) comp) { quicksort(&arr, 0, sizeof(arr), comp); } function number less(number x, number y) { return x < y; } public function void main() { number[] arr; for (number i = 0; i < 100; ++i) { arr[sizeof(arr)] = rnd(100); } trace(tostring(arr)); sort(&arr, less); trace(tostring(arr)); sort(&arr, greater); trace(tostring(arr)); }
هنا جزء C ++:
#include <iostream> #include "module.hpp" #include "standard_functions.hpp" int main() { std::string path = __FILE__; path = path.substr(0, path.find_last_of("/\\") + 1) + "test.stk"; using namespace stork; module m; add_standard_functions(m); m.add_external_function( "greater", std::function<number(number, number)>([](number x, number y){ return x > y; } )); auto s_main = m.create_public_function_caller<void>("main"); if (m.try_load(path.c_str(), &std::cerr)) { s_main(); } return 0; }
تتم إضافة الوظائف القياسية إلى الوحدة النمطية قبل التجميع ، ويتم استخدام وظائف trace
و rnd
من نص Stork النصي. يتم أيضًا إضافة الوظيفة greater
كعرض.
يتم تحميل البرنامج النصي من الملف "test.stk" الموجود في نفس المجلد مثل "main.cpp" (باستخدام تعريف المعالج المسبق __FILE__
) ، ثم يتم استدعاء الوظيفة main
.
في البرنامج النصي ، نقوم بإنشاء مصفوفة عشوائية ، يتم الفرز تصاعديًا باستخدام المقارنة less
، ثم بالترتيب التنازلي باستخدام المقارنة greater
، المكتوبة بلغة C ++.
يمكنك أن ترى أن الكود قابل للقراءة تمامًا لأي شخص يجيد لغة C (أو أي لغة برمجة مشتقة من C).
ما العمل التالي؟
هناك العديد من الميزات التي أود تنفيذها في Stork:
- الهياكل
- الطبقات والميراث
- المكالمات بين الوحدات
- وظائف لامدا
- الكائنات المكتوبة ديناميكيًا
يعد قلة الوقت والمكان أحد أسباب عدم تنفيذها بالفعل. سأحاول تحديث صفحة GitHub الخاصة بي بإصدارات جديدة لأنني أقوم بتنفيذ ميزات جديدة في أوقات فراغي.
تغليف
لقد أنشأنا لغة برمجة جديدة!
استغرق ذلك جزءًا كبيرًا من وقت فراغي في الأسابيع الستة الماضية ، لكن يمكنني الآن كتابة بعض السيناريوهات ورؤيتها قيد التشغيل. هذا ما كنت أفعله في الأيام القليلة الماضية ، خدش رأسي الأصلع في كل مرة تحطمت فيها بشكل غير متوقع. في بعض الأحيان ، كان خطأ صغيرًا ، وأحيانًا خطأ سيئًا. لكن في أوقات أخرى ، شعرت بالحرج لأنه كان قرارًا سيئًا كنت قد شاركته بالفعل مع العالم. لكن في كل مرة ، أقوم بإصلاح الترميز والاستمرار في ذلك.
في هذه العملية ، تعلمت ما if constexpr
، وهو ما لم أستخدمه من قبل. أصبحت أيضًا أكثر دراية بمراجع rvalue وإعادة التوجيه المثالية ، بالإضافة إلى الميزات الأصغر الأخرى لـ C ++ 17 التي لا أواجهها يوميًا.
الكود ليس مثاليًا - لن أقدم مثل هذا الادعاء أبدًا - لكنه جيد بما فيه الكفاية ، ويتبع في الغالب ممارسات البرمجة الجيدة. والأهم من ذلك أنها تعمل.
قد يبدو اتخاذ قرار بشأن تطوير لغة جديدة من الصفر أمرًا مجنونًا لشخص عادي ، أو حتى للمبرمج العادي ، ولكن هذا سبب إضافي للقيام بذلك وإثبات قدرتك على القيام بذلك. تمامًا مثل حل اللغز الصعب هو تمرين جيد للدماغ للحفاظ على لياقتك العقلية.
التحديات المملة شائعة في برامجنا اليومية ، حيث لا يمكننا اختيار الجوانب المثيرة للاهتمام فقط وعلينا القيام بعمل جاد حتى لو كان مملاً في بعض الأحيان. إذا كنت مطورًا محترفًا ، فإن أولويتك الأولى هي تقديم رمز عالي الجودة إلى صاحب العمل الخاص بك ووضع الطعام على الطاولة. قد يجعلك هذا في بعض الأحيان تتجنب البرمجة في أوقات فراغك ويمكن أن يثبط حماسك في أيام البرمجة المبكرة في المدرسة.
إذا لم تكن مضطرًا لذلك ، فلا تفقد هذا الحماس. اعمل على شيء ما إذا وجدته ممتعًا ، حتى لو تم بالفعل. ليس عليك تبرير سبب الاستمتاع ببعض المرح.
وإذا كان بإمكانك دمجها - ولو جزئيًا - في عملك المهني ، فهذا جيد لك! لا يملك الكثير من الناس هذه الفرصة.
سيتم تجميد رمز هذا الجزء من خلال فرع مخصص على صفحة GitHub الخاصة بي.