Stork, часть 4: Реализация заявлений и завершение

Опубликовано: 2022-03-11

В нашем стремлении создать облегченный язык программирования с использованием C++ мы начали с создания нашего токенизатора три недели назад, а затем реализовали оценку выражения в следующие две недели.

Теперь пришло время завершить и предоставить полный язык программирования, который не будет таким мощным, как зрелый язык программирования, но будет иметь все необходимые функции, в том числе очень небольшой размер.

Я нахожу забавным, как новые компании имеют разделы часто задаваемых вопросов на своих веб-сайтах, которые не отвечают на часто задаваемые вопросы, а на вопросы, которые они хотят задать. Я сделаю то же самое здесь. Люди, следящие за моей работой, часто спрашивают меня, почему Stork не компилируется в какой-то байт-код или хотя бы в какой-то промежуточный язык.

Почему Stork не компилируется в байт-код?

Я рад ответить на этот вопрос. Моя цель состояла в том, чтобы разработать небольшой скриптовый язык, который легко интегрировался бы с C++. У меня нет строгого определения «малого размера», но я представляю себе компилятор, который будет достаточно мал, чтобы его можно было переносить на менее мощные устройства, и не будет потреблять слишком много памяти при запуске.

С++ Аист

Я не заострял внимание на скорости, так как думаю, что вы будете кодировать на C++, если у вас есть срочная задача, но если вам нужна какая-то расширяемость, то такой язык, как Stork, может быть полезен.

Я не утверждаю, что нет других, лучших языков, способных решить подобную задачу (например, Lua). Было бы поистине трагично, если бы их не существовало, и я просто даю вам представление об использовании этого языка.

Поскольку он будет встроен в C++, я считаю удобным использовать некоторые существующие функции C++ вместо того, чтобы писать целую экосистему, которая будет служить той же цели. Не только это, но я также нахожу этот подход более интересным.

Как всегда, вы можете найти полный исходный код на моей странице GitHub. Теперь давайте поближе посмотрим на наш прогресс.

Изменения

До этой части Stork был частично законченным продуктом, поэтому я не смог увидеть все его недостатки и недостатки. Однако по мере того, как он приобретал более законченную форму, я изменил следующие вещи, представленные в предыдущих частях:

  • Функции больше не являются переменными. Теперь в compiler_context есть отдельный function_lookup . function_param_lookup переименован в param_lookup во избежание путаницы.
  • Я изменил способ вызова функций. В runtime_context есть метод call , который принимает std::vector аргументов, сохраняет старый индекс возвращаемого значения, помещает аргументы в стек, изменяет индекс возвращаемого значения, вызывает функцию, извлекает аргументы из стека, восстанавливает старый индекс возвращаемого значения и возвращает результат. Таким образом, нам не нужно хранить стек индексов возвращаемых значений, как раньше, потому что для этой цели служит стек C++.
  • Классы RAII, добавленные в compiler_context , которые возвращаются вызовами его функций-членов scope и function . Каждый из этих объектов создает новые local_identifier_lookup и param_identifier_lookup соответственно в своих конструкторах и восстанавливает старое состояние в деструкторе.
  • Класс RAII, добавленный в runtime_context , возвращаемый функцией-членом get_scope . Эта функция сохраняет размер стека в своем конструкторе и восстанавливает его в своем деструкторе.
  • Я удалил ключевое слово const и постоянные объекты в целом. Они могут быть полезны, но не являются абсолютно необходимыми.
  • Ключевое слово var удалено, так как в данный момент оно вообще не нужно.
  • Я добавил ключевое слово sizeof , которое будет проверять размер массива во время выполнения. Возможно, некоторые программисты на C++ сочтут выбор имени кощунственным, поскольку C++ sizeof запускается во время компиляции, но я выбрал это ключевое слово, чтобы избежать конфликта с некоторыми распространенными именами переменных, например, size .
  • Я добавил ключевое слово tostring , которое явно преобразует что-либо в 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 с объявлением в первом выражении. Мы также разрешаем операторы if и switch с выражением инициализации, как в C++17. Оператор if начинается с блока if , за которым следует ноль или более блоков elif и, необязательно, один блок else . Если бы переменная была объявлена ​​в выражении инициализации оператора 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 , но я не большой поклонник гигантских классов, и компилятор определенно был бы одним из таких классов. Передача лишнего аргумента, особенно тонкого, никому не помешает, и кто знает, может быть, однажды мы сможем распараллелить код.

Есть еще один интересный аспект компиляции, который я хотел бы здесь объяснить.

Если мы хотим поддерживать сценарий, в котором две функции вызывают друг друга, мы можем сделать это по-Си: разрешив предварительное объявление или задействовав две фазы компиляции.

Я выбрал второй подход. Когда определение функции будет найдено, мы проанализируем его тип и имя в объекте с именем 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 , компилятор вызывается рекурсивно, начиная с оператора блока, который представляет собой блок всей функции.

Вот абстрактный базовый класс для всех операторов:

 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() .

 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 , нуля или более блоков elif и одного блока else (который может быть пустым), оценивает каждое из своих выражений до тех пор, пока одно выражение не будет оценено как 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 и do_while_statement оба выполняют свои операторы тела, в то время как их выражение оценивается как 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 является объявлением переменной.

C++ Stork: Реализация операторов

Это все классы операторов, которые у нас есть. Они являются строительными блоками наших функций. Когда runtime_context , он сохраняет эти функции. Если функция объявлена ​​с ключевым словом public , ее можно вызвать по имени.

На этом основная функциональность Stork заканчивается. Все остальное, что я опишу, — это второстепенные мысли, которые я добавил, чтобы сделать наш язык более полезным.

Кортежи

Массивы являются однородными контейнерами, так как могут содержать элементы только одного типа. Если нам нужны разнородные контейнеры, на ум сразу приходят структуры.

Однако есть и более простые разнородные контейнеры: кортежи. Кортежи могут содержать элементы разных типов, но их типы должны быть известны во время компиляции. Это пример объявления кортежа в 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. Второй создает вызываемый объект, который можно использовать для вызова функции 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)); }

Вот часть С++:

 #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.