Stork,第 4 部分:实现语句和总结

已发表: 2022-03-11

在我们使用 C++ 创建轻量级编程语言的过程中,我们从三周前创建标记器开始,然后在接下来的两周内实现了表达式评估。

现在,是时候结束并交付一种完整的编程语言了,它不会像成熟的编程语言那么强大,但将具有所有必要的特性,包括非常小的占用空间。

我觉得有趣的是,新公司在其网站上的常见问题解答部分不回答经常被问到的问题,而是他们想被问到的问题。 我会在这里做同样的事情。 关注我工作的人经常问我为什么 Stork 不能编译成一些字节码或至少是一些中间语言。

为什么 Stork 不编译为字节码?

我很高兴回答这个问题。 我的目标是开发一种可以轻松与 C++ 集成的小型脚本语言。 我对“占用空间小”没有严格的定义,但我认为编译器应该足够小,可以移植到功能较弱的设备上,并且在运行时不会消耗太多内存。

C++ 鹳

我没有关注速度,因为我认为如果你有一个时间紧迫的任务,你会用 C++ 编写代码,但是如果你需要某种可扩展性,那么像 Stork 这样的语言可能会很有用。

我并不是说没有其他更好的语言可以完成类似的任务(例如,Lua)。 如果它们不存在,那将是真正的悲剧,我只是让您了解这种语言的用例。

由于它将嵌入到 C++ 中,我发现使用 C++ 的一些现有特性而不是编写一个用于类似目的的整个生态系统很方便。 不仅如此,我还发现这种方法更有趣。

与往常一样,您可以在我的 GitHub 页面上找到完整的源代码。 现在,让我们仔细看看我们的进展。

变化

到目前为止,Stork 是一个部分完整的产品,所以我无法看到它的所有缺点和缺陷。 但是,由于它的形状更完整,我更改了前面部分介绍的以下内容:

  • 函数不再是变量。 现在compiler_context中有一个单独的function_lookupfunction_param_lookup被重命名为param_lookup以避免混淆。
  • 我改变了调用函数的方式。 runtime_context中有一个call方法,它接受参数的std::vector ,存储旧的返回值索引,将参数压入堆栈,更改返回值索引,调用函数,从堆栈中弹出参数,恢复旧的返回值索引,以及返回结果。 这样,我们就不必像以前那样保留返回值索引的堆栈,因为 C++ 堆栈服务于这个目的。
  • compiler_context中添加的 RAII 类,由对其成员函数scopefunction的调用返回。 这些对象中的每一个都分别在其构造函数中创建新的local_identifier_lookupparam_identifier_lookup ,并在析构函数中恢复旧状态。
  • runtime_context中添加的 RAII 类,由成员函数get_scope返回。 该函数将堆栈大小存储在其构造函数中并在其析构函数中恢复它。
  • 我一般删除了const关键字和常量对象。 它们可能有用,但不是绝对必要的。
  • var关键字已删除,因为目前根本不需要它。
  • 我添加了sizeof关键字,它将在运行时检查数组大小。 也许一些 C++ 程序员会发现名称选择是亵渎神明的,因为 C++ sizeof在编译时运行,但我选择该关键字是为了避免与一些常见的变量名称冲突 - 例如size
  • 我添加了tostring关键字,它将任何内容显式转换为string 。 它不能是函数,因为我们不允许函数重载。
  • 各种不太有趣的变化。

句法

由于我们使用的语法与 C 及其相关编程语言非常相似,因此我将仅提供可能不清楚的细节。

变量类型声明如下:

  • void ,仅用于函数返回类型
  • number
  • string
  • T[]是一个包含T类型元素的数组
  • R(P1,...,Pn)是一个函数,它返回类型R并接收类型为P1Pn的参数。 如果通过引用传递,则每种类型都可以使用&作为前缀。

函数声明如下: [public] function R name(P1 p1, … Pn pn)

因此,它必须以function为前缀。 如果它以public为前缀,则可以从 C++ 调用它。 如果函数没有返回值,它将评估为其返回类型的默认值。

我们允许for -loop 在第一个表达式中声明。 我们还允许带有初始化表达式的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 ,显示了我们正在解析的上下文中允许的流更改命令( continuebreakreturn )。 如果编译语句是某个compiler类的成员函数,我可以将这些信息保留在对象中,但我不是庞大的类的忠实粉丝,编译器肯定是这样的一个类。 传递一个额外的参数,尤其是一个细小的参数,不会伤害任何人,谁知道呢,也许有一天我们将能够并行化代码。

我想在这里解释编译的另一个有趣的方面。

如果我们想支持两个函数相互调用的场景,我们可以使用 C 方式:允许前向声明或有两个编译阶段。

我选择了第二种方法。 找到函数定义后,我们会将其类型和名称解析为名为incomplete_function的对象。 然后,我们将跳过它的主体,不做解释,只计算花括号的嵌套级别,直到我们关闭第一个花括号。 我们将在这个过程中收集令牌,将它们保存在incomplete_function的函数中,并将函数标识符添加到compiler_context中。

一旦我们传递了整个文件,我们将完整地编译每个函数,以便可以在运行时调用它们。 这样,每个函数都可以调用文件中的任何其他函数,并且可以访问任何全局变量。

全局变量可以通过调用相同的函数来初始化,一旦这些函数访问未初始化的变量,我们就会立即陷入旧的“鸡和蛋”问题。

如果发生这种情况,可以通过抛出runtime_exception来解决问题——这只是因为我很好。 弗兰奇,访问违规是编写此类代码的最少惩罚。

全球范围

有两种实体可以出现在全局范围内:

  • 全局变量
  • 职能

每个全局变量都可以使用返回正确类型的表达式进行初始化。 为每个全局变量创建初始化程序。

每个初始值设定项都返回lvalue ,因此它们充当全局变量的构造函数。 如果没有为全局变量提供表达式,则构造默认初始值设定项。

这是runtime_context中的initialize成员函数:

 void runtime_context::initialize() { _globals.clear(); for (const auto& initializer : _initializers) { _globals.emplace_back(initializer->evaluate(*this)); } }

它是从构造函数中调用的。 它清除全局变量容器,因为它可以被显式调用,以重置runtime_context状态。

正如我之前提到的,我们需要检查是否访问了未初始化的全局变量。 因此,这是全局变量访问器:

 variable_ptr& runtime_context::global(int idx) { runtime_assertion( idx < _globals.size(), "Uninitialized global variable access" ); return _globals[idx]; }

如果第一个参数的计算结果为false ,则runtime_assertion会抛出带有相应消息的runtime_error

每个函数都实现为捕获单个语句的 lambda,然后使用函数接收的runtime_context对其进行评估。

功能范围

while语句编译中可以看出,编译器是递归调用的,从 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(); };

静态创建函数是不言自明的,我编写它们是为了防止非零break_level和与flow_type::f_break不同的类型的不合逻辑flow

现在, 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 。 由于breakcontinuereturn都不能成为表达式的一部分,因此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_statementreturn_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_statementdo_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_statementfor_statement_declare的实现与while_statementdo_statement类似。 它们继承自for_statement_base类,该类执行大部分逻辑。 当for循环的第一部分是变量声明时,会创建for_statement_declare

C++ Stork:实现语句

这些都是我们拥有的语句类。 它们是我们功能的组成部分。 创建runtime_context时,它会保留这些功能。 如果函数使用关键字public声明,则可以按名称调用。

到此结束 Stork 的核心功能。 我将描述的其他所有内容都是我添加的事后想法,以使我们的语言更有用。

元组

数组是同类容器,因为它们只能包含单一类型的元素。 如果我们想要异构容器,就会立即想到结构。

然而,还有更琐碎的异构容器:元组。 元组可以保留不同类型的元素,但它们的类型必须在编译时知道。 这是 Stork 中元组声明的示例:

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

这声明了numberstring对并对其进行了初始化。

初始化列表也可用于初始化数组。 当初始化列表中的表达式类型与变量类型不匹配时,会出现编译错误。

由于数组被实现为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(); ... };

函数loadtry_load将从给定路径加载和编译 Stork 脚本。 首先,其中一个可以抛出stork::error ,但第二个会捕获它并将其打印在输出上(如果提供)。

函数reset_globals将重新初始化全局变量。

函数add_external_functionscreate_public_function_caller应该在编译之前被调用。 第一个添加了一个可以从 Stork 调用的 C++ 函数。 第二个创建可用于从 C++ 调用 Stork 函数的可调用对象。 如果在 Stork 脚本编译期间公共函数类型与R(Args…)不匹配,则会导致编译时错误。

我添加了几个可以添加到 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; }

在编译之前将标准函数添加到模块中,并使用 Stork 脚本中的函数tracerndgreater的功能也被添加为展示。

该脚本是从文件“test.stk”中加载的,该文件与“main.cpp”位于同一文件夹中(通过使用__FILE__预处理器定义),然后调用函数main

在脚本中,我们生成一个随机数组,使用比较器less升序排序,然后使用比较器greater的降序排序,用 C++ 编写。

您可以看到代码对于任何精通 C(或任何从 C 派生的编程语言)的人来说都是完全可读的。

接下来做什么?

我想在 Stork 中实现许多功能:

  • 结构
  • 类和继承
  • 模块间调用
  • Lambda 函数
  • 动态类型对象

缺乏时间和空间是我们尚未实施它们的原因之一。 当我在业余时间实现新功能时,我将尝试用新版本更新我的 GitHub 页面。

包起来

我们创造了一种新的编程语言!

在过去的六周里,这占用了我大部分空闲时间,但我现在可以编写一些脚本并查看它们运行。 这几天我就是这么干的,每次出乎意料的坠毁都挠着我的光头。 有时,这是一个小错误,有时是一个讨厌的错误。 然而,在其他时候,我感到很尴尬,因为这是一个我已经与世界分享的错误决定。 但每次,我都会修复并继续编码。

在这个过程中,我了解了我以前从未使用过的if constexpr 。 我也更加熟悉右值引用和完美转发,以及我每天都不会遇到的 C++17 的其他较小功能。

代码并不完美——我永远不会这么说——但它已经足够好了,而且它主要遵循良好的编程实践。 最重要的是 - 它有效。

决定从头开始开发一门新语言对于普通人甚至普通程序员来说可能听起来很疯狂,但这样做更有理由向自己证明你可以做到。 就像解决一个难题一样,是一种很好的大脑锻炼,可以保持精神健康。

枯燥的挑战在我们的日常编程中很常见,因为我们不能只挑选其中有趣的方面,即使有时很无聊,也必须认真工作。 如果你是一名专业的开发人员,你的首要任务是向你的雇主提供高质量的代码并将食物摆在桌面上。 这有时会使您避免在业余时间进行编程,并且会降低您早期编程学生时代的热情。

如果您不必这样做,请不要失去这种热情。 如果你觉得某件事很有趣,那就去做吧,即使它已经完成了。 你不必证明有一些乐趣的理由。

如果你能把它——甚至是部分——融入你的专业工作,对你有好处! 没有多少人有这个机会。

这部分的代码将被我的 GitHub 页面上的一个专用分支冻结。