นกกระสา ตอนที่ 4: การปฏิบัติตามคำแถลงและบทสรุป

เผยแพร่แล้ว: 2022-03-11

ในภารกิจของเราในการสร้างภาษาการเขียนโปรแกรมขนาดเล็กโดยใช้ C++ เราเริ่มต้นโดยการสร้าง tokenizer เมื่อสามสัปดาห์ก่อน จากนั้นจึงนำการประเมินนิพจน์ไปใช้ในสองสัปดาห์ถัดไป

ตอนนี้ ถึงเวลาแล้วที่จะสรุปและนำเสนอภาษาการเขียนโปรแกรมที่สมบูรณ์ซึ่งจะไม่มีประสิทธิภาพเท่ากับภาษาการเขียนโปรแกรมสำหรับผู้ใหญ่ แต่จะมีคุณสมบัติที่จำเป็นทั้งหมด ซึ่งรวมถึงพื้นที่ขนาดเล็กมาก

ฉันคิดว่ามันตลกดีที่บริษัทใหม่มีส่วนคำถามที่พบบ่อยบนเว็บไซต์ของพวกเขาซึ่งไม่ตอบคำถามที่ถูกถามบ่อยแต่เป็นคำถามที่พวกเขาต้องการถาม ฉันจะทำเช่นเดียวกันที่นี่ ผู้คนที่ติดตามงานของฉันมักจะถามฉันว่าทำไม Stork ไม่คอมไพล์เป็น bytecode หรืออย่างน้อยก็บางภาษาระดับกลาง

ทำไม Stork ไม่คอมไพล์เป็น Bytecode?

ฉันยินดีที่จะตอบคำถามนี้ เป้าหมายของฉันคือการพัฒนาภาษาสคริปต์ขนาดเล็กที่สามารถรวมเข้ากับ C ++ ได้อย่างง่ายดาย ฉันไม่มีคำจำกัดความที่เข้มงวดของ "small-footprint" แต่ฉันนึกภาพคอมไพเลอร์ที่มีขนาดเล็กพอที่จะเปิดใช้งานการพกพาไปยังอุปกรณ์ที่มีประสิทธิภาพน้อยกว่าและจะไม่ใช้หน่วยความจำมากเกินไปเมื่อรัน

C++ นกกระสา

ฉันไม่ได้เน้นที่ความเร็ว เพราะฉันคิดว่าคุณจะเขียนโค้ดในภาษา C++ หากคุณมีงานที่ต้องใช้เวลามาก แต่ถ้าคุณต้องการความสามารถในการขยาย ภาษาอย่าง Stork อาจมีประโยชน์

ฉันไม่อ้างว่าไม่มีภาษาอื่นที่ดีกว่าที่สามารถทำงานที่คล้ายกันได้สำเร็จ (เช่น Lua) มันคงน่าสลดใจจริง ๆ หากไม่มีมัน และฉันแค่ให้แนวคิดเกี่ยวกับกรณีการใช้งานของภาษานี้แก่คุณ

เนื่องจากมันจะถูกฝังลงใน C++ ฉันพบว่ามันสะดวกที่จะใช้คุณสมบัติที่มีอยู่ของ C++ แทนที่จะเขียนระบบนิเวศทั้งหมดที่มีจุดประสงค์คล้ายกัน ไม่เพียงเท่านั้น แต่ฉันยังพบว่าแนวทางนี้น่าสนใจยิ่งขึ้น

เช่นเคย คุณสามารถค้นหาซอร์สโค้ดแบบเต็มได้ในหน้า GitHub ของฉัน ตอนนี้ มาดูความก้าวหน้าของเรากันดีกว่า

การเปลี่ยนแปลง

จนถึงส่วนนี้ นกกระสาเป็นผลิตภัณฑ์ที่สมบูรณ์เพียงบางส่วน ดังนั้นฉันจึงไม่สามารถเห็นข้อเสียและข้อบกพร่องทั้งหมดของมันได้ อย่างไรก็ตาม เนื่องจากมันมีรูปร่างที่สมบูรณ์มากขึ้น ฉันจึงเปลี่ยนสิ่งต่อไปนี้ที่นำมาใช้ในส่วนก่อนหน้า:

  • ฟังก์ชั่นไม่ใช่ตัวแปรอีกต่อไป ตอนนี้มี 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 ใหม่ตามลำดับในตัวสร้างและกู้คืนสถานะเก่าใน destructor
  • เพิ่มคลาส RAII ใน runtime_context ส่งคืนโดยฟังก์ชันสมาชิก get_scope ฟังก์ชั่นนั้นเก็บขนาดสแต็กในตัวสร้างและกู้คืนในตัวทำลาย
  • ฉันลบคำหลัก const และวัตถุคงที่โดยทั่วไป อาจมีประโยชน์แต่ไม่จำเป็นอย่างยิ่ง
  • ลบคีย์เวิร์ด var เนื่องจากไม่จำเป็นต้องใช้เลย
  • ฉันเพิ่มคำหลัก sizeof ซึ่งจะตรวจสอบขนาดอาร์เรย์ในรันไทม์ บางทีโปรแกรมเมอร์ C++ บางคนอาจพบว่าตัวเลือกชื่อดูหมิ่นศาสนา เนื่องจาก C++ sizeof ทำงานในเวลาคอมไพล์ แต่ฉันเลือกคำหลักนั้นเพื่อหลีกเลี่ยงความขัดแย้งกับชื่อตัวแปรทั่วไปบางตัว - ตัวอย่างเช่น size
  • ฉันเพิ่มคำ tostring ซึ่งแปลงอะไรเป็น string อย่างชัดเจน ไม่สามารถเป็นฟังก์ชันได้ เนื่องจากเราไม่อนุญาตให้มีฟังก์ชันโอเวอร์โหลด
  • การเปลี่ยนแปลงที่น่าสนใจน้อยกว่าต่างๆ

ไวยากรณ์

เนื่องจากเราใช้ไวยากรณ์ที่คล้ายกับ C และภาษาการเขียนโปรแกรมที่เกี่ยวข้องกันมาก ฉันจะให้รายละเอียดที่อาจไม่ชัดเจน

การประกาศประเภทตัวแปรมีดังนี้:

  • void ใช้สำหรับฟังก์ชัน return type . เท่านั้น
  • 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 -statement ด้วยนิพจน์การเริ่มต้น เช่นเดียวกับใน C++17 if -statement เริ่มต้นด้วย if -block ตามด้วยศูนย์ elif -blocks หรือมากกว่า และทางเลือก else -block หากตัวแปรถูกประกาศในนิพจน์การเริ่มต้นของ if -statement ก็จะปรากฏให้เห็นในแต่ละบล็อกเหล่านั้น

เราอนุญาตให้ใช้ตัวเลขเสริมหลังคำสั่ง 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 จากนั้น ( จากนั้นจะสร้างนิพจน์ตัวเลข (เราไม่มีบูลีน) จากนั้นจึงแยกวิเคราะห์ )

หลังจากนั้นจะรวบรวมคำสั่ง block ที่อาจอยู่ภายใน { และ } หรือไม่ (ใช่ ฉันอนุญาตการบล็อกแบบ single-statement) และจะสร้างคำสั่ง 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 พร้อมข้อความที่เกี่ยวข้อง

แต่ละฟังก์ชันจะถูกนำไปใช้เป็นแลมบ์ดาที่รวบรวมคำสั่งเดียว ซึ่งจะถูกประเมินด้วย 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 ได้ เนื่องจากไม่สามารถ break หรือ continue หรือ 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() continue_statement

 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 ทั้งสอง return 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, zero หรือมากกว่า elif -blocks และ else อัน -block (ซึ่งอาจว่างเปล่า) ประเมินแต่ละนิพจน์จนกว่านิพจน์หนึ่งจะประเมินเป็น 1 จากนั้นรันบล็อกนั้นและส่งคืนผลการดำเนินการ หากไม่มีนิพจน์ใดมีค่า 1 นิพจน์จะส่งคืนการดำเนินการของบล็อกสุดท้าย ( else )

if_declare_statement เป็นคำสั่งที่มีการประกาศเป็นส่วนแรกของ if-clause มันผลักตัวแปรที่ประกาศทั้งหมดไปยังสแต็กแล้วรันคลาสฐาน ( 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 และ do_while_statement ทั้งคู่ดำเนินการคำสั่ง body ในขณะที่นิพจน์ประเมินเป็น 1 หากการดำเนินการส่งคืน 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 คือการประกาศตัวแปร

C ++ Stork: การใช้คำสั่ง

เหล่านี้เป็นคลาสคำสั่งทั้งหมดที่เรามี พวกเขาเป็นส่วนสำคัญของหน้าที่ของเรา เมื่อ runtime_context ถูกสร้างขึ้น มันจะเก็บฟังก์ชันเหล่านั้นไว้ หากฟังก์ชันถูกประกาศด้วยคีย์เวิร์ด public สามารถเรียกตามชื่อได้

ที่สรุปการทำงานหลักของนกกระสา ทุกสิ่งทุกอย่างที่ฉันจะอธิบายคือความหลังที่ฉันเพิ่มเข้าไปเพื่อให้ภาษาของเรามีประโยชน์มากขึ้น

ทูเปิลส์

อาร์เรย์เป็นคอนเทนเนอร์ที่เป็นเนื้อเดียวกัน เนื่องจากสามารถมีองค์ประกอบของประเภทเดียวเท่านั้น หากเราต้องการภาชนะที่ต่างกัน โครงสร้างจะต้องนึกถึงทันที

อย่างไรก็ตาม มีคอนเทนเนอร์ที่แตกต่างกันเล็กน้อย: ทูเพิล ทูเปิลส์สามารถเก็บอิลิเมนต์ประเภทต่างๆ ไว้ได้ แต่จะต้องทราบประเภทของพวกมันในเวลารวบรวม นี่คือตัวอย่างการประกาศทูเพิลใน Stork:

 [number, string] t = {22321, "Siveric"};

นี่เป็นการประกาศคู่ของ number และ string และเริ่มต้นมัน

รายการเริ่มต้นสามารถใช้เพื่อเริ่มต้นอาร์เรย์ได้เช่นกัน เมื่อชนิดของนิพจน์ในรายการเริ่มต้นไม่ตรงกับชนิดของตัวแปร จะเกิดข้อผิดพลาดของคอมไพเลอร์

เนื่องจากอาร์เรย์ถูกนำไปใช้เป็นคอนเทนเนอร์ของ variable_ptr เราจึงได้รับการใช้งานรันไทม์ของทูเพิลฟรี เป็นเวลารวบรวมเมื่อเรารับรองประเภทตัวแปรที่มีอยู่อย่างถูกต้อง

โมดูล

เป็นการดีที่จะซ่อนรายละเอียดการใช้งานจากผู้ใช้ 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 อันที่สองสร้างอ็อบเจกต์ callable ที่สามารถใช้เรียกฟังก์ชัน 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);

ตัวอย่าง

นี่คือตัวอย่างของสคริปต์ Stork:

 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-references และการส่งต่อที่สมบูรณ์แบบมากขึ้น ตลอดจนคุณลักษณะอื่นๆ ที่เล็กกว่าของ C++17 ที่ฉันไม่พบทุกวัน

โค้ดไม่สมบูรณ์แบบ ฉันไม่เคยอ้างสิทธิ์แบบนั้นเลย แต่ก็ดีพอ และส่วนใหญ่เป็นไปตามแนวทางปฏิบัติด้านการเขียนโปรแกรมที่ดี และที่สำคัญที่สุด - มันได้ผล

การตัดสินใจพัฒนาภาษาใหม่ตั้งแต่ต้นอาจฟังดูบ้าสำหรับคนทั่วไป หรือแม้แต่โปรแกรมเมอร์ทั่วๆ ไป แต่ก็เป็นเหตุผลมากกว่าสำหรับการทำและพิสูจน์ตัวเองว่าคุณทำได้ เช่นเดียวกับการไขปริศนาที่ยากคือการฝึกสมองที่ดีเพื่อให้มีสุขภาพจิตที่ดี

ความท้าทายที่น่าเบื่อเป็นเรื่องปกติในการเขียนโปรแกรมประจำวันของเรา เนื่องจากเราไม่สามารถเลือกเฉพาะด้านที่น่าสนใจของมันได้ และต้องทำงานอย่างจริงจังถึงแม้จะน่าเบื่อในบางครั้ง หากคุณเป็นนักพัฒนามืออาชีพ สิ่งสำคัญอันดับแรกของคุณคือส่งรหัสคุณภาพสูงให้นายจ้างของคุณและนำอาหารมาวางบนโต๊ะ บางครั้งสิ่งนี้อาจทำให้คุณหลีกเลี่ยงการเขียนโปรแกรมในเวลาว่าง และอาจทำให้ความกระตือรือร้นในการเขียนโปรแกรมสมัยเรียนในโรงเรียนของคุณลดลง

ถ้าไม่จำเป็น อย่าสูญเสียความกระตือรือร้นนั้นไป ทำงานบางอย่างหากคุณพบว่ามันน่าสนใจ แม้ว่ามันจะทำเสร็จแล้วก็ตาม คุณไม่จำเป็นต้องหาเหตุผลมาสนุกกับมัน

และถ้าคุณสามารถรวมมันไว้ในงานอาชีพของคุณได้—แม้เพียงบางส่วน—ก็ดีสำหรับคุณ! น้อยคนนักที่จะมีโอกาสนั้น

รหัสสำหรับส่วนนี้จะถูกระงับด้วยสาขาเฉพาะในหน้า GitHub ของฉัน