นกกระสา ตอนที่ 4: การปฏิบัติตามคำแถลงและบทสรุป
เผยแพร่แล้ว: 2022-03-11ในภารกิจของเราในการสร้างภาษาการเขียนโปรแกรมขนาดเล็กโดยใช้ C++ เราเริ่มต้นโดยการสร้าง tokenizer เมื่อสามสัปดาห์ก่อน จากนั้นจึงนำการประเมินนิพจน์ไปใช้ในสองสัปดาห์ถัดไป
ตอนนี้ ถึงเวลาแล้วที่จะสรุปและนำเสนอภาษาการเขียนโปรแกรมที่สมบูรณ์ซึ่งจะไม่มีประสิทธิภาพเท่ากับภาษาการเขียนโปรแกรมสำหรับผู้ใหญ่ แต่จะมีคุณสมบัติที่จำเป็นทั้งหมด ซึ่งรวมถึงพื้นที่ขนาดเล็กมาก
ฉันคิดว่ามันตลกดีที่บริษัทใหม่มีส่วนคำถามที่พบบ่อยบนเว็บไซต์ของพวกเขาซึ่งไม่ตอบคำถามที่ถูกถามบ่อยแต่เป็นคำถามที่พวกเขาต้องการถาม ฉันจะทำเช่นเดียวกันที่นี่ ผู้คนที่ติดตามงานของฉันมักจะถามฉันว่าทำไม Stork ไม่คอมไพล์เป็น bytecode หรืออย่างน้อยก็บางภาษาระดับกลาง
ทำไม Stork ไม่คอมไพล์เป็น Bytecode?
ฉันยินดีที่จะตอบคำถามนี้ เป้าหมายของฉันคือการพัฒนาภาษาสคริปต์ขนาดเล็กที่สามารถรวมเข้ากับ C ++ ได้อย่างง่ายดาย ฉันไม่มีคำจำกัดความที่เข้มงวดของ "small-footprint" แต่ฉันนึกภาพคอมไพเลอร์ที่มีขนาดเล็กพอที่จะเปิดใช้งานการพกพาไปยังอุปกรณ์ที่มีประสิทธิภาพน้อยกว่าและจะไม่ใช้หน่วยความจำมากเกินไปเมื่อรัน
ฉันไม่ได้เน้นที่ความเร็ว เพราะฉันคิดว่าคุณจะเขียนโค้ดในภาษา 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 คือการประกาศตัวแปร

เหล่านี้เป็นคลาสคำสั่งทั้งหมดที่เรามี พวกเขาเป็นส่วนสำคัญของหน้าที่ของเรา เมื่อ 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 ของฉัน