นกกระสา: วิธีสร้างภาษาโปรแกรมใน C++

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

ส่วนที่ 1: Tokenizer

ในชุดนี้ เราจะพัฒนาภาษาสคริปต์ใหม่และอธิบายกระบวนการนั้นทีละขั้นตอน

คำถามแรกที่ผุดขึ้นมาในหัวของผู้อ่านที่สงสัยคือ: “เราจำเป็นต้องมีภาษาโปรแกรมใหม่จริง ๆ หรือไม่”

เราต้องการภาษาการเขียนโปรแกรมใหม่จริงหรือ

เพื่อตอบคำถามนี้ ฉันจะปล่อยให้ตัวเองพูดนอกเรื่องเล็กน้อย

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

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

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

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

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

ซอร์สโค้ดที่สมบูรณ์สำหรับทุกสิ่งที่อธิบายไว้ที่นี่มีให้บริการฟรีที่ GitHub

สุดท้ายนี้ เพื่อตอบคำถามจากชื่อย่อหน้านี้—ไม่ จริงๆ แล้ว เราไม่จำเป็นต้องมีภาษาโปรแกรมใหม่ แต่เนื่องจากเราพยายามสาธิตวิธีสร้างภาษาโปรแกรมใน C++ เราจะสร้างภาษาสำหรับการสาธิต .

ผู้ช่วยตัวน้อยของ Tokenizer

ฉันไม่รู้ว่าโปรแกรมเมอร์คนอื่นๆ ประสบปัญหาเดียวกันเป็นประจำหรือไม่ แต่ฉันประสบปัญหานี้ค่อนข้างบ่อย:

ฉันต้องการคอนเทนเนอร์คีย์-ค่าที่ควรดึงค่าอย่างรวดเร็วในเวลาลอการิทึม อย่างไรก็ตาม เมื่อฉันเริ่มต้นคอนเทนเนอร์ ฉันไม่ต้องการเพิ่มค่าใหม่เข้าไป ดังนั้น std::map<Key, Value> (หรือ std::unordered_map<Key, Value> ) จึงเกินความจำเป็น เนื่องจากช่วยให้แทรกได้อย่างรวดเร็วเช่นกัน

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

ตัวเลือกที่สองคือ std::vector<std::pair<Key,Value> > จัดเรียงหลังจากการแทรก ปัญหาเดียวของวิธีการนั้นคือความสามารถในการอ่านโค้ดได้น้อยกว่า เนื่องจากเราต้องจำไว้ว่าเวกเตอร์นั้นถูกจัดเรียง ดังนั้นฉันจึงพัฒนาคลาสขนาดเล็กที่รับรองข้อจำกัดนั้น

(ทุกฟังก์ชัน คลาส ฯลฯ ถูกประกาศในเนมสเปซ stork ฉันจะละเว้นเนมสเปซนั้นเพื่อให้สามารถอ่านได้)

 template <typename Key, typename Value> class lookup { public: using value_type = std::pair<Key, Value>; using container_type = std::vector<value_type>; private: container_type _container; public: using iterator = typename container_type::const_iterator; using const_iterator = iterator; lookup(std::initializer_list<value_type> init) : _container(init) { std::sort(_container.begin(), _container.end()); } lookup(container_type container) : _container(std::move(container)) { std::sort(_container.begin(), _container.end()); } const_iterator begin() const { return _container.begin(); } const_iterator end() const { return _container.end(); } template <typename K> const_iterator find(const K& key) const { const_iterator it = std::lower_bound( begin(), end(), key, [](const value_type& p, const K& key) { return p.first < key; } ); return it != end() && it->first == key ? it : end(); } size_t size() const { return _container.size(); } };

อย่างที่คุณเห็น การใช้งานคลาสนี้ค่อนข้างง่าย ฉันไม่ต้องการใช้วิธีการที่เป็นไปได้ทั้งหมด เพียงแค่วิธีที่เราต้องการ คอนเทนเนอร์พื้นฐานคือ vector จึงสามารถเริ่มต้นได้ด้วย vector ที่เติมไว้ล่วงหน้า หรือด้วย initializer_list

tokenizer จะอ่านอักขระจากสตรีมอินพุต ในขั้นตอนนี้ของโปรเจ็กต์ มันยากสำหรับฉันที่จะตัดสินใจว่าอินพุตสตรีมจะเป็นอะไร ดังนั้นฉันจะใช้ std::function แทน

 using get_character = std::function<int()>;

ฉันจะใช้ข้อตกลงที่รู้จักกันดีจากฟังก์ชันสตรีมสไตล์ C เช่น getc ซึ่งจะคืนค่า int แทนที่จะเป็น char รวมถึงตัวเลขติดลบเพื่อส่งสัญญาณการสิ้นสุดของไฟล์

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

 class push_back_stream { private: const get_character& _input; std::stack<int> _stack; size_t _line_number; size_t _char_index; public: push_back_stream(const get_character& input); int operator()(); void push_back(int c); size_t line_number() const; size_t char_index() const; };

เพื่อประหยัดพื้นที่ ฉันจะละเว้นรายละเอียดการใช้งาน ซึ่งคุณสามารถหาได้ในหน้า GitHub ของฉัน

อย่างที่คุณเห็น push_back_stream เริ่มต้นจากฟังก์ชัน get_character operator() ใช้เพื่อดึงอักขระตัวถัดไปและ push_back ใช้เพื่อส่งคืนอักขระกลับไปที่สตรีม line_number และ char_number เป็นวิธีการอำนวยความสะดวกที่ใช้สำหรับรายงานข้อผิดพลาด

โปรดทราบว่า char_index ไม่ใช่ดัชนีของอักขระในบรรทัดปัจจุบัน แต่โดยรวม มิฉะนั้น เราจะต้องเก็บอักขระที่ผ่านมาทั้งหมดไว้ในคอนเทนเนอร์บางส่วนเพื่อใช้งานฟังก์ชัน push_back อย่างถูกต้อง

โทเค็นที่สงวนไว้

โทเค็นที่สงวนไว้

tokenizer เป็นส่วนประกอบคอมไพเลอร์ระดับต่ำสุด ต้องอ่านสัญญาณอินพุตและโทเค็น "ถุยออก" โทเค็นที่เราสนใจมีสี่ประเภท:

  • โทเค็นที่จองไว้
  • ตัวระบุ
  • ตัวเลข
  • เครื่องสาย

เราไม่สนใจความคิดเห็น ดังนั้น tokenizer จะ "กิน" พวกเขาโดยไม่ต้องแจ้งให้ใครทราบ

เพื่อให้มั่นใจถึงความน่าดึงดูดใจและความนิยมของภาษานี้ เราจะใช้ไวยากรณ์ที่เหมือน C ที่รู้จักกันดี มันใช้งานได้ดีกับ C, C++, JavaScript, Java, C# และ Objective-C ดังนั้นจึงต้องใช้ได้กับ Stork เช่นกัน ในกรณีที่คุณต้องการหลักสูตรทบทวน คุณสามารถอ่านบทความก่อนหน้าของเราซึ่งครอบคลุมแหล่งข้อมูลการเรียนรู้ C/C++

นี่คือการแจงนับโทเค็นที่สงวนไว้:

 enum struct reserved_token { inc, dec, add, sub, concat, mul, div, idiv, mod, bitwise_not, bitwise_and, bitwise_or, bitwise_xor, shiftl, shiftr, assign, add_assign, sub_assign, concat_assign, mul_assign, div_assign, idiv_assign, mod_assign, and_assign, or_assign, xor_assign, shiftl_assign, shiftr_assign, logical_not, logical_and, logical_or, eq, ne, lt, gt, le, ge, question, colon, comma, semicolon, open_round, close_round, open_curly, close_curly, open_square, close_square, kw_if, kw_else, kw_elif, kw_switch, kw_case, kw_default, kw_for, kw_while, kw_do, kw_break, kw_continue, kw_return, kw_var, kw_fun, kw_void, kw_number, kw_string, };

สมาชิกการแจงนับที่นำหน้าด้วย “kw_” เป็นคีย์เวิร์ด ฉันต้องใส่คำนำหน้าเนื่องจากมักจะเหมือนกับคำหลัก C++ ที่ไม่มีคำนำหน้าคือโอเปอเรเตอร์

เกือบทั้งหมดปฏิบัติตามอนุสัญญา C สิ่งที่ไม่ได้คือ:
- concat และ concat_assign ( .. และ ..= ) ซึ่งจะใช้สำหรับต่อ
- idiv และ idiv_assign ( \ และ \= ) ซึ่งจะใช้สำหรับหารจำนวนเต็ม
- kw_var สำหรับการประกาศตัวแปร
- kw_fun สำหรับการประกาศฟังก์ชัน
- kw_number สำหรับตัวแปรตัวเลข
- kw_string สำหรับตัวแปรสตริง

เราจะเพิ่มคำสำคัญเพิ่มเติมตามความจำเป็น

มีคำหลักใหม่หนึ่งคำที่ควรอธิบาย: kw_elif ฉันเชื่อมั่นว่าบล็อกข้อความเดียว (ไม่มีวงเล็บปีกกา) นั้นไม่คุ้มค่า ฉันไม่ได้ใช้มัน (และฉันไม่รู้สึกว่ามีอะไรขาดหายไป) ยกเว้นสองครั้ง:

  1. เมื่อฉันเผลอกดอัฒภาคทันทีหลังจาก a for , while , หรือ if คำสั่งก่อนบล็อก ถ้าฉันโชคดี มันจะส่งคืนข้อผิดพลาดเวลาคอมไพล์ แต่บางครั้ง มันส่งผลให้เกิดคำสั่ง if-statement และบล็อกที่ดำเนินการเสมอ โชคดีที่หลายปีที่ผ่านมา ฉันได้เรียนรู้จากความผิดพลาดของตัวเอง ดังนั้นมันจึงเกิดขึ้นไม่บ่อยนัก ในที่สุดสุนัขของ Pavlov ก็ได้เรียนรู้เช่นกัน
  2. เมื่อฉันมี "ห่วงโซ่" if-statement ดังนั้นฉันจึงมี if-block แล้วก็ else-if-blocks อย่างน้อยหนึ่งรายการและอีกทางเลือกหนึ่งคือ else-block ในทางเทคนิค เมื่อฉันเขียน else if นั่นเป็นบล็อก else มีคำสั่งเดียว ซึ่งก็คือ if-statement

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

มีฟังก์ชันตัวช่วยสองแบบที่ส่งคืนโทเค็นที่สงวนไว้:

 std::optional<reserved_token> get_keyword(std::string_view word); std::optional<reserved_token> get_operator(push_back_stream& stream);

ฟังก์ชัน get_keyword ส่งคืนคีย์เวิร์ดที่ไม่บังคับจากคำที่ส่งผ่าน “คำ” นั้นคือลำดับของตัวอักษร ตัวเลข และขีดล่าง โดยขึ้นต้นด้วยตัวอักษรหรือขีดล่าง จะส่งกลับ reserved_token หากคำนั้นเป็นคีย์เวิร์ด มิฉะนั้น tokenizer จะถือว่ามันเป็นตัวระบุ

ฟังก์ชัน get_operator พยายามอ่านอักขระให้ได้มากที่สุด ตราบใดที่ลำดับเป็นตัวดำเนินการที่ถูกต้อง หากอ่านมากกว่านี้ จะยกเลิกการอ่านอักขระพิเศษทั้งหมดที่อ่านหลังจากโอเปอเรเตอร์ที่ยาวที่สุดที่รู้จัก

เพื่อการใช้งานฟังก์ชันทั้งสองอย่างมีประสิทธิภาพ เราจำเป็นต้องมีการค้นหาสองครั้งระหว่าง string_view และ reserved_keyword

 const lookup<std::string_view, reserved_token> operator_token_map { {"++", reserved_token::inc}, {"--", reserved_token::dec}, {"+", reserved_token::add}, {"-", reserved_token::sub}, {"..", reserved_token::concat}, /*more exciting operators*/ }; const lookup<std::string_view, reserved_token> keyword_token_map { {"if", reserved_token::kw_if}, {"else", reserved_token::kw_else}, {"elif", reserved_token::kw_elif}, {"switch", reserved_token::kw_switch}, {"case", reserved_token::kw_case}, {"default", reserved_token::kw_default}, {"for", reserved_token::kw_for}, {"while", reserved_token::kw_while}, {"do", reserved_token::kw_do}, {"break", reserved_token::kw_break}, {"continue", reserved_token::kw_continue}, {"return", reserved_token::kw_return}, {"var", reserved_token::kw_var}, {"fun", reserved_token::kw_fun}, {"void", reserved_token::kw_void}, {"number", reserved_token::kw_number}, {"string", reserved_token::kw_string} };

การใช้งาน get_keyword นั้นตรงไปตรงมาโดยสิ้นเชิง แต่สำหรับ get_operator เราจำเป็นต้องมีตัวเปรียบเทียบแบบกำหนดเองที่จะเปรียบเทียบอักขระที่กำหนดกับตัวดำเนินการตัวเลือก โดยคำนึงถึงเฉพาะอักขระตัวที่ n

 class maximal_munch_comparator{ private: size_t _idx; public: maximal_munch_comparator(size_t idx) : _idx(idx) { } bool operator()(char l, char r) const { return l < r; } bool operator()( std::pair<std::string_view, reserved_token> l, char r ) const { return l.first.size() <= _idx || l.first[_idx] < r; } bool operator()( char l, std::pair<std::string_view, reserved_token> r ) const { return r.first.size() > _idx && l < r.first[_idx]; } bool operator()( std::pair<std::string_view, reserved_token> l, std::pair<std::string_view, reserved_token> r ) const { return r.first.size() > _idx && ( l.first.size() < _idx || l.first[_idx] < r.first[_idx] ); } };

นั่นคือตัวเปรียบเทียบคำศัพท์ทั่วไปที่พิจารณาเฉพาะอักขระที่ตำแหน่ง idx แต่ถ้าสตริงสั้นกว่า จะถือว่าอักขระนั้นมีค่าว่างที่ตำแหน่ง idx ซึ่งน้อยกว่าอักขระอื่นๆ

นี่คือการใช้งาน get_operator ซึ่งจะทำให้คลาส maximal_munch_operator ชัดเจนขึ้น:

 std::optional<reserved_token> get_operator(push_back_stream& stream) { auto candidates = std::make_pair( operator_token_map.begin(), operator_token_map.end() ); std::optional<reserved_token> ret; size_t match_size = 0; std::stack<int> chars; for (size_t idx = 0; candidates.first != candidates.second; ++idx) { chars.push(stream()); candidates = std::equal_range( candidates.first, candidates.second, char(chars.top()), maximal_munch_comparator(idx) ); if ( candidates.first != candidates.second && candidates.first->first.size() == idx + 1 ) { match_size = idx + 1; ret = candidates.first->second; } } while (chars.size() > match_size) { stream.push_back(chars.top()); chars.pop(); } return ret; }

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

เมื่อใดก็ตามที่ช่วงไม่ว่าง เราจะตรวจสอบว่าองค์ประกอบแรกในช่วงไม่มีอักขระอีกหรือไม่ (หากมีองค์ประกอบดังกล่าว องค์ประกอบดังกล่าวจะอยู่ที่จุดเริ่มต้นของช่วงเสมอเมื่อมีการจัดเรียงการค้นหา) ในกรณีนั้น เรามีผู้ดำเนินการทางกฎหมายที่ตรงกัน ตัวดำเนินการดังกล่าวที่ยาวที่สุดคือตัวดำเนินการที่เราส่งคืน เราจะยังไม่ได้อ่านอักขระทั้งหมดที่เราอ่านหลังจากนั้น

Tokenizer

เนื่องจากโทเค็นต่างกัน โทเค็นจึงเป็นคลาสสะดวกที่รวม std::variant ประเภทโทเค็นที่แตกต่างกัน กล่าวคือ:

  • โทเค็นที่จองไว้
  • ตัวระบุ
  • ตัวเลข
  • สตริง
  • สิ้นสุดไฟล์
 class token { private: using token_value = std::variant<reserved_token, identifier, double, std::string, eof>; token_value _value; size_t _line_number; size_t _char_index; public: token(token_value value, size_t line_number, size_t char_index); bool is_reserved_token() const; bool is_identifier() const; bool is_number() const; bool is_string() const; bool is_eof() const; reserved_token get_reserved_token() const; std::string_view get_identifier() const; double get_number() const; std::string_view get_string() const; size_t get_line_number() const; size_t get_char_index() const; };

identifier เป็นเพียงคลาสที่มีสมาชิกประเภท std::string เพียงตัวเดียว มีไว้เพื่อความสะดวกเนื่องจากในความคิดของฉัน std::variant นั้นสะอาดกว่าหากทางเลือกทั้งหมดเป็นประเภทที่แตกต่างกัน

ตอนนี้ เราสามารถเขียน tokenizer ได้ มันจะเป็นหนึ่งฟังก์ชันที่จะยอมรับ push_back_stream และส่งคืนโทเค็นถัดไป

เคล็ดลับคือการใช้โค้ดสาขาต่างๆ ตามประเภทอักขระของอักขระตัวแรกที่เราอ่าน

  • ถ้าเราอ่านอักขระ end-of-file เราจะกลับมาจากฟังก์ชัน
  • ถ้าเราอ่านช่องว่าง เราจะข้ามมันไป
  • หากเราอ่านอักขระที่เป็นตัวอักษรและตัวเลข (ตัวอักษร ตัวเลข หรือขีดล่าง) เราจะอ่านอักขระต่อเนื่องกันของประเภทนั้น (เราจะอ่านจุดด้วยหากอักขระตัวแรกเป็นตัวเลข) จากนั้น หากอักขระตัวแรกเป็นตัวเลข เราจะพยายามแยกวิเคราะห์ลำดับเป็นตัวเลข มิฉะนั้น เราจะใช้ฟังก์ชัน get_keyword เพื่อตรวจสอบว่าเป็นคีย์เวิร์ดหรือตัวระบุหรือไม่
  • หากเราอ่านเครื่องหมายคำพูด เราจะถือว่ามันเป็นสตริง โดยที่อักขระที่ใช้ Escape ไม่ได้หนีออกจากเครื่องหมายนั้น
  • หากเราอ่านเครื่องหมายทับ ( / ) เราจะตรวจสอบว่าอักขระตัวต่อไปคือเครื่องหมายทับหรือเครื่องหมายดอกจัน ( * ) และเราจะข้ามบรรทัด/บล็อกความคิดเห็นในกรณีนั้น
  • มิฉะนั้น เราจะใช้ฟังก์ชัน get_operator

นี่คือการนำฟังก์ชัน tokenize ไปใช้งาน ฉันจะละเว้นรายละเอียดการใช้งานของฟังก์ชันที่เรียก

 token tokenize(push_back_stream& stream) { while (true) { size_t line_number = stream.line_number(); size_t char_index = stream.char_index(); int c = stream(); switch (get_character_type(c)) { case character_type::eof: return {eof(), line_number, char_index}; case character_type::space: continue; case character_type::alphanum: stream.push_back(c); return fetch_word(stream); case character_type::punct: switch (c) { case '"': return fetch_string(stream); case '/': { char c1 = stream(); switch(c1) { case '/': skip_line_comment(stream); continue; case '*': skip_block_comment(stream); continue; default: stream.push_back(c1); } } default: stream.push_back(c); return fetch_operator(stream); } break; } } }

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

ข้อยกเว้น

พี่ชายของฉันเคยพูดจาโผงผางเรื่องข้อยกเว้นครั้งหนึ่ง:

“มีคนสองประเภท: พวกที่มีข้อยกเว้นกับคนที่ต้องจับพวกเขา ฉันมักจะอยู่ในกลุ่มที่สองที่น่าเศร้าเสมอ”

ฉันเห็นด้วยกับจิตวิญญาณของข้อความนั้น ฉันไม่ชอบข้อยกเว้นเป็นพิเศษ และการโยนมันทิ้งไปจะทำให้โค้ดใด ๆ ดูแลรักษาและอ่านยากขึ้น เกือบตลอดเวลา.

ฉันตัดสินใจที่จะยกเว้น (ตั้งใจเล่นสำนวนที่ไม่ดี) กับกฎนั้น มันสะดวกมากที่จะโยนข้อยกเว้นจากคอมไพเลอร์เพื่อผ่อนคลายจากส่วนลึกของการรวบรวม

นี่คือการใช้งานข้อยกเว้น:

 class error: public std::exception { private: std::string _message; size_t _line_number; size_t _char_index; public: error(std::string message, size_t line_number, size_t char_index) noexcept; const char* what() const noexcept override; size_t line_number() const noexcept; size_t char_index() const noexcept; };

อย่างไรก็ตาม ฉันสัญญาว่าจะจับข้อยกเว้นทั้งหมดในโค้ดระดับบนสุด ฉันยังเพิ่มสมาชิก line_number และ char_index สำหรับการพิมพ์สวย ๆ และฟังก์ชั่นที่ทำ:

 void format_error( const error& err, get_character source, std::ostream& output );

ห่อ

ที่สรุปส่วนแรกของซีรีส์ของเรา บางทีอาจจะไม่น่าตื่นเต้นเกินไป แต่ตอนนี้เรามี tokenizer ที่มีประโยชน์ พร้อมด้วยการจัดการข้อผิดพลาดในการแยกวิเคราะห์ขั้นพื้นฐาน ทั้งสองเป็นส่วนสำคัญในการสร้างสิ่งที่น่าสนใจมากขึ้นที่ฉันจะเขียนเกี่ยวกับในบทความต่อ ๆ ไป

ฉันหวังว่าคุณจะได้รับแนวคิดดีๆ จากโพสต์นี้ และถ้าคุณต้องการสำรวจรายละเอียด ไปที่หน้า GitHub ของฉัน


อ่านเพิ่มเติมในบล็อก Toptal Engineering:

  • วิธีการเขียนล่ามตั้งแต่เริ่มต้น