在本系列的第3部分中, 我们的轻量级编程语言将最终运行。它不会是图灵完备的, 不会强大的, 但是它将能够计算表达式, 甚至可以调用用C ++编写的外部函数。
我将尝试尽可能详细地描述该过程, 主要是因为这是本博客系列的目的, 而且是我自己的文档, 因为在这一部分中, 事情有些复杂。
在第二篇文章发表之前, 我已经开始对此部分进行编码, 但是后来发现表达式解析器应该是一个独立的组件, 应有自己的博客文章。
这样, 再加上一些臭名昭著的编程技术, 这部分内容可能不会变得异常庞大, 但是, 一些读者很可能会指出上述编程技术, 并且想知道为什么我必须使用它们。
为什么我们使用宏?
当我获得了在不同项目上以及与不同人员一起工作的编程经验时, 我了解到开发人员往往很教条-可能是因为这样比较容易。
编程的第一个原则是goto语句是坏的, 邪恶的和可怕的。我能理解这种情感的来源, 并且在有人使用goto语句的绝大多数情况下, 我都同意这种观点。通常可以避免这种情况, 而可以编写更具可读性的代码。
但是, 不能否认使用goto语句可以轻松完成C ++中的内循环。替代方法(需要bool变量或专用函数)可能比那些在原理上属于禁止编程技术的代码的可读性差。
第二个教条, 仅与C和C ++开发人员有关, 是宏是坏的, 邪恶的, 糟糕的, 并且基本上, 灾难正在等待发生。这个例子几乎总是伴随着这个例子:
#define max(a, b) ((a) > (b) ? (a) : (b))
...
int x = 3;
int z = 2;
int y = max(x++, z);
然后有一个问题:这段代码后x的值是多少, 答案是5, 因为x递增两次, 在?运算符的每一侧增加一个。
唯一的问题是在这种情况下没有人使用宏。如果宏在普通函数可以正常工作的情况下使用, 则它们是有害的, 尤其是在它们假装为函数的情况下, 因此用户不会意识到它们的副作用。但是, 我们不会将它们用作函数, 并且将使用大写字母作为其名称, 以使其明显不是函数。我们将无法正确调试它们, 这很糟糕, 但是我们会继续这样做, 因为替代方法是将同一代码复制粘贴数十次, 比宏更容易出错。解决该问题的一种方法是编写代码生成器, 但是如果已经在C ++中嵌入了一个代码生成器, 为什么还要编写它呢?
编程中的教条几乎总是不好的。我在这里谨慎使用”几乎”是为了避免递归地落入我刚刚设置的教条陷阱中。
你可以在此处找到该部分的代码和所有宏。
变量
在上一部分中, 我提到了Stork不会被编译成二进制或类似于汇编语言的任何东西, 但我也说过它将是一种静态类型的语言。因此, 它将被编译, 但是将在可以执行的C ++对象中。稍后将变得更加清楚, 但是现在, 让我们声明所有变量将都是对象。
由于我们要将它们保留在全局变量容器中或堆栈中, 因此一种方便的方法是定义基类并从中继承。
class variable;
using variable_ptr = std::shared_ptr<variable>;
class variable: public std::enable_shared_from_this<variable> {
private:
variable(const variable&) = delete;
void operator=(const variable&) = delete;
protected:
variable() = default;
public:
virtual ~variable() = default;
virtual variable_ptr clone() const = 0;
template <typename T>
T static_pointer_downcast() {
return std::static_pointer_cast<
variable_impl<typename T::element_type::value_type>
>(shared_from_this());
}
};
如你所见, 它非常简单, 执行深度复制的函数clone是除析构函数之外唯一的虚拟成员函数。
由于我们将始终通过shared_ptr使用此类的对象, 因此从std :: enable_shared_from_this继承它是有意义的, 因此我们可以轻松地从中获取共享指针。为了方便起见, 此处使用了static_pointer_downcast函数, 因为我们经常不得不从此类向下转换至其实现。
该类的实际实现是variable_impl, 并通过其持有的类型进行参数化。将为我们将使用的四种类型实例化它:
using number = double;
using string = std::shared_ptr<std::string>;
using array = std::deque<variable_ptr>;
using function = std::function<void(runtime_context&)>;
我们将使用double作为我们的数字类型。字符串是引用计数的, 因为它们将是不可变的, 以便在按值传递它们时启用某些优化。数组将是std :: deque, 因为它很稳定, 我们只需要注意runtime_context是一个类, 它在运行时保存有关程序内存的所有相关信息。我们稍后再讲。
也经常使用以下定义:
using lvalue = variable_ptr;
using lnumber = std::shared_ptr<variable_impl<number>>;
using lstring = std::shared_ptr<variable_impl<string>>;
using larray = std::shared_ptr<variable_impl<array>>;
using lfunction = std::shared_ptr<variable_impl<function>>;
此处使用的” l”缩写为” lvalue”。只要我们有某种类型的左值, 我们就会使用指向variable_impl的共享指针。
运行时上下文
在运行时, 内存状态保存在类runtime_context中。
class runtime_context{
private:
std::vector<variable_ptr> _globals;
std::deque<variable_ptr> _stack;
std::stack<size_t> _retval_idx;
public:
runtime_context(size_t globals);
variable_ptr& global(int idx);
variable_ptr& retval();
variable_ptr& local(int idx);
void push(variable_ptr v);
void end_scope(size_t scope_vars);
void call();
variable_ptr end_function(size_t params);
};
它使用全局变量的计数进行初始化。
- _globals保留所有全局变量。使用具有绝对索引的全局成员函数访问它们。
- _stack保留局部变量和函数参数, 并且_retval_idx顶部的整数将绝对索引保留在当前返回值的_stack中。
- 通过函数retval访问返回值, 而通过传递相对于当前返回值的索引, 使用函数local访问局部变量和函数自变量。在这种情况下, 函数参数具有负索引。
- push函数将变量添加到堆栈中, 而end_scope从堆栈中删除传递的变量数量。
- 调用函数将堆栈大小调整为一, 并将_stack中最后一个元素的索引推入_retval_idx。
- end_function从堆栈中删除返回值和传递的参数数量, 并且还返回删除的返回值。
如你所见, 我们将不实施任何低级内存管理, 而将利用本机(C ++)内存管理, 这是理所当然的。至少到目前为止, 我们也不会实现任何堆分配。
有了runtime_context, 我们终于有了本部分中央和最困难组件所需的所有构造块。
表达方式
为了充分说明我将在此处介绍的复杂解决方案, 在向你介绍此方法之前, 我将向你简要介绍我几次失败的尝试。
最简单的方法是将每个表达式评估为variable_ptr并具有以下虚拟基类:
class expression {
...
public:
variable_ptr evaluate(runtime_context& context) const = 0;
lnumber evaluate_lnumber(runtime_context& context) const {
return evaluate(context)->static_pointer_downcast<lnumber>();
}
lstring evaluate_lstring(runtime_context& context) const {
return evaluate(context)->static_pointer_downcast<lstring>();
}
number evaluate_number(runtime_context& context) const {
return evaluate_lnumber(context)->value;
}
string evaluate_string(runtime_context& context) const {
return evaluate_lstring(context)->value;
}
...
};
using expression_ptr = std::unique_ptr<expression>;
然后, 我们将为每个操作从此类继承, 例如加法, 串联, 函数调用等。例如, 这将是加法表达式实现:
class add_expression: public expression {
private:
expression_ptr _expr1;
expression_ptr _expr2;
public:
...
variable_ptr evaluate(runtime_context& context) const override{
return std::make_shared<variable_impl<number> >(
_expr1->evaluate_number(context) +
_expr2->evaluate_number(context)
);
}
...
};
因此, 我们需要评估双方(_expr1和_expr2), 将它们相加, 然后构造variable_impl <number>。
我们可以放心地转换变量, 因为我们在编译时检查了变量的类型, 所以这不是问题。但是, 最大的问题是我们为返回对象的堆分配支付的性能损失, 从理论上讲, 这是不需要的。我们这样做是为了满足虚函数声明。在Stork的第一个版本中, 当我们从函数返回数字时, 我们将受到惩罚。我可以忍受, 但不能使用简单的预增量表达式进行堆分配。
然后, 我尝试使用从通用库继承的特定于类型的表达式:
class expression {
...
public:
virtual void evaluate(runtime_context& context) const = 0;
...
};
class lvalue_expression: public virtual expression {
...
public:
virtual lvalue evaluate_lvalue(runtime_context& context) const = 0;
void evaluate(runtime_context& context) const override {
evaluate_lvalue(context);
}
...
};
using lvalue_expression_ptr = std::unique_ptr<lvalue_expression>;
class number_expression: public virtual expression {
...
public:
virtual number evaluate_number(runtime_context& context) const = 0;
void evaluate(runtime_context& context) const override {
evaluate_number(context);
}
...
};
using number_expression_ptr = std::unique_ptr<number_expression>;
class lnumber_expression: public lvalue_expression, public number_expression {
...
public:
virtual lnumber evaluate_lnumber(runtime_context& context) const = 0;
lvalue evaluate_lvalue(runtime_context& context) const override {
return evaluate_lnumber(context);
}
number evaluate_number(runtime_context& context) const override {
return evaluate_lnumber(context)->value;
}
void evaluate(runtime_context& context) const override {
return evaluate_lnumber(context);
}
...
};
using lnumber_expression_ptr = std::unique_ptr<lnumber_expression>;
这只是层次结构的一部分(仅用于数字), 我们已经遇到菱形的问题(该类继承具有相同基类的两个类)。
幸运的是, C ++提供了虚拟继承, 通过在继承的类中保留指向基类的指针, 它可以从基类继承。因此, 如果类B和C实际上是从A继承的, 而类D是从B和C继承的, 那么D中将只有A的一个副本。
不过, 在这种情况下, 我们需要付出很多代价-性能和无法从A贬低, 仅举几例-但这仍然是我第一次使用虚拟继承的机会。我的生活。
现在, 加法表达式的实现看起来更加自然:
class add_expression: public number_expression {
private:
number_expression_ptr _expr1;
number_expression_ptr _expr2;
public:
...
number evaluate_number(runtime_context& context) const override{
return _expr1->evaluate_number(context) +
_expr2->evaluate_number(context);
}
...
};
在语法方面, 没有什么要问的了, 这很自然。但是, 如果任何内部表达式是左值表达式, 则将需要两个虚拟函数调用来对其求值。也不完美, 但也不可怕。
让我们将字符串添加到此混音中, 看看它能带给我们什么:
class string_expression: public virtual expression {
...
public:
virtual string evaluate_string(runtime_context& context) const = 0;
void evaluate(runtime_context& context) const override {
evaluate_string(context);
}
...
};
using string_expression_ptr = std::unique_ptr<string_expression>;
由于我们希望数字可以转换为字符串, 因此我们需要从string_expression继承number_expression。
class number_expression: public string_expression {
...
public:
virtual number evaluate_number(runtime_context& context) const = 0;
string evaluate_string(runtime_context& context) const override {
return tostring(evaluate_number(context));
}
void evaluate(runtime_context& context) const override {
evaluate_number(context);
}
...
};
using number_expression_ptr = std::unique_ptr<number_expression>;
我们幸免于难, 但是我们必须重新覆盖评估虚拟方法, 否则由于从数字到字符串的不必要转换, 我们将面临严重的性能问题。
因此, 事情显然变得很丑陋, 而且我们的设计几乎无法幸免, 因为我们没有两种类型的表达式应该相互转换(两种方式)。如果是这种情况, 或者我们尝试进行任何形式的循环转换, 则我们的层次结构将无法处理。毕竟, 等级制应该反映的是弱关系, 是一种关系, 而不是可转换关系。
所有这些失败的尝试导致我进行了复杂但又正确的设计。首先, 只有一个基本类别对我们而言并不重要。我们需要可以评估为void的表达式类, 但是如果我们可以在编译时区分void表达式和其他类型的表达式, 则无需在运行时在它们之间进行转换。因此, 我们将使用表达式的返回类型参数化基类。
这是该类的完整实现:
template <typename R>
class expression {
expression(const expression&) = delete;
void operator=(const expression&) = delete;
protected:
expression() = default;
public:
using ptr = std::unique_ptr<const expression>;
virtual R evaluate(runtime_context& context) const = 0;
virtual ~expression() = default;
};
每个表达式求值将只有一个虚拟函数调用(当然, 我们将必须递归调用它), 并且由于我们不编译为二进制代码, 因此效果相当不错。唯一要做的就是在类型之间进行转换(如果允许)。
为此, 我们将使用返回类型对每个表达式进行参数化, 并从相应的基类继承它。然后, 在评估函数中, 我们将评估结果转换为该函数的返回值。
例如, 这是我们的加法表达式:
template <typename R>
class add_expression: public expression<R> {
...
R evaluate(runtime_context& context) const override{
return convert<R>(
_expr1->evaluate(context) +
_expr2->evaluate(context)
);
}
...
};
要编写”转换”功能, 我们需要一些基础设施:
template<class V, typename T>
struct is_boxed {
static const bool value = false;
};
template<typename T>
struct is_boxed<std::shared_ptr<variable_impl<T> >, T> {
static const bool value = true;
};
string convert_to_string(number n) {
std::string str
if (n == int(n)) {
str = std::to_string(int(n));
} else {
str = std::to_string(n);
}
return std::make_shared<std::string>(std::move(str));
}
string convert_to_string(const lnumber& v) {
return convert_to_string(v->value);
}
is_boxed结构是具有内部常量值的类型特征, 当(且仅当)第一个参数是使用第二种类型参数化的variable_impl的共享指针时, 该属性的值才为true。
即使在较旧的C ++版本中, 也可以实现convert函数, 但是C ++ 17中有一个非常有用的语句if constexpr, 它在编译时评估条件。如果计算结果为false, 即使会导致编译时错误, 也会完全删除该块。否则, 它将删除else块。
template<typename To, typename From>
auto convert(From&& from) {
if constexpr(std::is_convertible<From, To>::value) {
return std::forward<From>(from);
} else if constexpr(is_boxed<From, To>::value) {
return unbox(std::forward<From>(from));
} else if constexpr(std::is_same<To, string>::value) {
return convert_to_string(from);
} else {
static_assert(std::is_void<To>::value);
}
}
尝试阅读此功能:
- 如果可以在C ++中转换, 则进行转换(这适用于up_variable指针转换)。
- 如果已装箱, 请取消装箱。
- 如果目标类型是字符串, 则转换为字符串。
- 不执行任何操作, 并检查目标是否无效。
我认为, 这比基于SFINAE的旧语法更具可读性。
我将提供表达式类型的简要概述, 并省略一些技术细节, 以使其保持合理的简短。
表达式树中有三种叶子表达式:
- 全局变量表达
- 局部变量表达
- 常数表达
template<typename R, typename T>
class global_variable_expression: public expression<R> {
private:
int _idx;
public:
global_variable_expression(int idx) :
_idx(idx)
{
}
R evaluate(runtime_context& context) const override {
return convert<R>(
context.global(_idx)
->template static_pointer_downcast<T>()
);
}
};
除了返回类型外, 还使用变量类型对其进行参数化。局部变量的处理方式类似, 这是常量的类:
template<typename R, typename T>
class constant_expression: public expression<R> {
private:
T _c;
public:
constant_expression(T c) :
_c(std::move(c))
{
}
R evaluate(runtime_context& context) const override {
return convert<R>(_c);
}
};
在这种情况下, 我们立即在构造函数中转换常量。
这被用作我们大多数表达式的基类:
template<class O, typename R, typename... Ts>
class generic_expression: public expression<R> {
private:
std::tuple<typename expression<Ts>::ptr...> _exprs;
template<typename... Exprs>
R evaluate_tuple(
runtime_context& context, const Exprs&... exprs
) const {
return convert<R>(O()(
std::move(exprs->evaluate(context))...)
);
}
public:
generic_expression(typename expression<Ts>::ptr... exprs) :
_exprs(std::move(exprs)...)
{
}
R evaluate(runtime_context& context) const override {
return std::apply(
[&](const auto&... exprs){
return this->evaluate_tuple(context, exprs...);
}, _exprs
);
}
};
第一个参数是将被实例化并被调用以求值的函子类型。其余类型是子表达式的返回类型。
为了减少样板代码, 我们定义了三个宏:
#define UNARY_EXPRESSION(name, code)\
struct name##_op {\
template <typename T1> \
auto operator()(T1 t1) {\
code;\
}\
};\
template<typename R, typename T1>\
using name##_expression = generic_expression<name##_op, R, T1>;
#define BINARY_EXPRESSION(name, code)\
struct name##_op {\
template <typename T1, typename T2>\
auto operator()(T1 t1, T2 t2) {\
code;\
}\
};\
template<typename R, typename T1, typename T2>\
using name##_expression = generic_expression<name##_op, R, T1, T2>;
#define TERNARY_EXPRESSION(name, code)\
struct name##_op {\
template <typename T1, typename T2, typename T3>\
auto operator()(T1 t1, T2 t2, T3 t3) {\
code;\
}\
};\
template<typename R, typename T1, typename T2, typename T3>\
using name##_expression = generic_expression<name##_op, R, T1, T2, T3>;
请注意, operator()定义为模板, 尽管通常不必这样做。以相同的方式定义所有表达式比提供参数类型作为宏参数要容易得多。
现在, 我们可以定义大多数表达式。例如, 这是/ =的定义:
BINARY_EXPRESSION(div_assign, t1->value /= t2;
return t1;
);
我们可以使用这些宏定义几乎所有的表达式。例外是定义了参数求值顺序的运算符(逻辑&&和||, 三元(?)和逗号(, )运算符), 数组索引, 函数调用和param_expression, 它们会克隆参数以将其传递给函数按价值。
这些的实现没有什么复杂的。函数调用实现是最复杂的, 因此我将在这里进行解释:
template<typename R, typename T>
class call_expression: public expression<R>{
private:
expression<function>::ptr _fexpr;
std::vector<expression<lvalue>::ptr> _exprs;
public:
call_expression(
expression<function>::ptr fexpr, std::vector<expression<lvalue>::ptr> exprs
):
_fexpr(std::move(fexpr)), _exprs(std::move(exprs))
{
}
R evaluate(runtime_context& context) const override {
std::vector<variable_ptr> params;
params.reserve(_exprs.size());
for (size_t i = 0; i < _exprs.size(); ++i) {
params.push_back(_exprs[i]->evaluate(context));
}
function f = _fexpr->evaluate(context);
for (size_t i = params.size(); i > 0; --i) {
context.push(std::move(params[i-1]));
}
context.call();
f(context);
if constexpr (std::is_same<R, void>::value) {
context.end_function(_exprs.size());
} else {
return convert<R>(
context.end_function(
_exprs.size()
)->template static_pointer_downcast<T>()
);
}
}
};
它通过将所有求值的参数压入其堆栈并调用call函数来准备runtime_context。然后, 它调用求值的第一个参数(它是函数本身), 并返回end_function方法的返回值。我们也可以在这里看到if constexpr语法的用法。它使我们不必为返回void的函数编写整个类的专业化知识。
现在, 我们拥有与运行时期间可用的表达式相关的所有内容。剩下的唯一事情就是从已解析的表达式树(在上一篇博客文章中进行了描述)到表达式树的转换。
表情生成器
为避免混淆, 让我们命名语言开发周期的不同阶段:
- 元编译时:C ++编译器运行的阶段
- 编译时:Stork编译器运行的阶段
- 运行时:Stork脚本运行的阶段
这是表达式构建器的伪代码:
function build_expression(nodeptr n, compiler_context context) {
if (n is constant) {
return constant_expression(n.value);
} else if (n is identifier) {
id_info info = context.find(n.value);
if (context.is_global(info)) {
return global_variable_expression(info.index);
} else {
return local_variable_expression(info.index);
}
} else { //operation
switch (n->value) {
case preinc:
return preinc_expression(
build_expression(n->child[0])
);
...
case add:
return add_expression(
build_expression(n->child[0]), build_expression(n->child[1])
);
...
case call:
return call_expression(
n->child[0], //function
n->child[1], //arg0
...
n->child[k+1], //argk
);
}
}
}
除了必须处理所有操作外, 这似乎是一种简单的算法。
如果有效, 那就太好了, 但事实并非如此。对于初学者, 我们需要指定函数的返回类型, 这里显然不固定, 因为返回类型取决于我们要访问的节点的类型。节点类型在编译时是已知的, 但是返回类型应该在元编译时是已知的。
在上一篇文章中, 我没有提到进行动态类型检查的语言的优势。在这种语言中, 上面显示的伪代码几乎可以在字面上实现。现在, 我很清楚动态类型语言的优点。最好的即刻业障。
幸运的是, 我们知道顶级表达式的类型-它取决于编译的上下文, 但是我们知道其类型而无需解析表达式树。例如, 如果我们有for循环:
for (expression1; expression2; expression3)
...
第一个和第三个表达式的返回类型为空, 因为我们对它们的评估结果不做任何事情。但是, 第二个表达式具有类型编号, 因为我们正在将其与零进行比较, 以便确定是否停止循环。
如果我们知道与节点操作相关的表达式的类型, 则通常将确定其子表达式的类型。
例如, 如果表达式(expression1)+ =(expression2)具有类型lnumber, 则意味着expression1也具有该类型, 而expression2也具有类型编号。
但是, 表达式(expression1)<(expression2)始终具有类型编号, 但是其子表达式可以具有类型编号或类型字符串。对于此表达式, 我们将检查两个节点是否均为数字。如果是这样, 我们将构建expression1和expression2作为expression <number>。否则, 它们将为expression <string>类型。
我们还必须考虑和处理另一个问题。
想象一下是否需要构建类型编号的表达式。然后, 如果遇到串联运算符, 我们将无法返回任何有效值。我们知道它不会发生, 因为我们在构建表达式树时已经检查了类型(在上一部分中), 但这意味着我们无法编写带有返回类型参数化的模板函数, 因为它将具有无效的分支, 具体取决于在该返回类型上。
一种方法是使用constexpr将函数按返回类型进行拆分, 但效率不高, 因为如果多个分支中存在相同的操作, 我们将不得不重复其代码。在这种情况下, 我们可以编写单独的函数。
实施的解决方案根据节点类型拆分功能。在每个分支中, 我们将检查该分支类型是否可转换为函数返回类型。如果不是这样, 我们将抛出编译器错误, 因为它永远都不会发生, 但是对于如此强烈的要求, 代码太复杂了。我可能犯了一个错误。
我们使用以下不言自明的类型特征结构来检查可转换性:
template<typename From, typename To>
struct is_convertible {
static const bool value =
std::is_convertible<From, To>::value ||
is_boxed<From, To>::value ||
(
std::is_same<To, string>::value &&
(
std::is_same<From, number>::value ||
std::is_same<From, lnumber>::value
)
);
};
拆分之后, 代码几乎很简单。我们可以从原始表达式类型语义上转换为我们要构建的表达式类型, 并且在元编译时没有错误。
但是, 有很多样板代码, 因此为了减少它, 我非常依赖宏。
template<typename R>
class expression_builder{
private:
using expression_ptr = typename expression<R>::ptr;
static expression_ptr build_void_expression(
const node_ptr& np, compiler_context& context
);
static expression_ptr build_number_expression(
const node_ptr& np, compiler_context& context
);
static expression_ptr build_lnumber_expression(
const node_ptr& np, compiler_context& context
);
static expression_ptr build_string_expression(
const node_ptr& np, compiler_context& context
);
static expression_ptr build_lstring_expression(
const node_ptr& np, compiler_context& context
);
static expression_ptr build_array_expression(
const node_ptr& np, compiler_context& context
);
static expression_ptr build_larray_expression(
const node_ptr& np, compiler_context& context
);
static expression_ptr build_function_expression(
const node_ptr& np, compiler_context& context
);
static expression_ptr build_lfunction_expression(
const node_ptr& np, compiler_context& context
);
public:
static expression_ptr build_expression(
const node_ptr& np, compiler_context& context
) {
return std::visit(overloaded{
[&](simple_type st){
switch (st) {
case simple_type::number:
if (np->is_lvalue()) {
RETURN_EXPRESSION_OF_TYPE(lnumber);
} else {
RETURN_EXPRESSION_OF_TYPE(number);
}
case simple_type::string:
if (np->is_lvalue()) {
RETURN_EXPRESSION_OF_TYPE(lstring);
} else {
RETURN_EXPRESSION_OF_TYPE(string);
}
case simple_type::nothing:
RETURN_EXPRESSION_OF_TYPE(void);
}
}, [&](const function_type& ft) {
if (np->is_lvalue()) {
RETURN_EXPRESSION_OF_TYPE(lfunction);
} else {
RETURN_EXPRESSION_OF_TYPE(function);
}
}, [&](const array_type& at) {
if (np->is_lvalue()) {
RETURN_EXPRESSION_OF_TYPE(larray);
} else {
RETURN_EXPRESSION_OF_TYPE(array);
}
}
}, *np->get_type_id());
}
};
函数build_expression是这里唯一的公共函数。它在节点类型上调用函数std :: visit。该函数将传递的函子应用于变量, 在过程中将其解耦。你可以在此处阅读更多有关它以及重载的函子的信息。
宏RETURN_EXPRESSION_OF_TYPE调用私有函数来构建表达式, 如果无法进行转换, 则抛出异常:
#define RETURN_EXPRESSION_OF_TYPE(T)\
if constexpr(is_convertible<T, R>::value) {\
return build_##T##_expression(np, context);\
} else {\
throw expression_builder_error();\
return expression_ptr();\
}
我们必须在else分支中返回空指针, 因为在无法进行转换的情况下, 编译器无法知道函数的返回类型。否则, std :: visit要求所有重载函数具有相同的返回类型。
例如, 有一个函数使用字符串作为返回类型来构建表达式:
static expression_ptr build_string_expression(
const node_ptr& np, compiler_context& context
) {
if (std::holds_alternative<std::string>(np->get_value())) {
return std::make_unique<constant_expression<R, string>>(
std::make_shared<std::string>(
std::get<std::string>(np->get_value())
)
);
}
CHECK_IDENTIFIER(lstring);
switch (std::get<node_operation>(np->get_value())) {
CHECK_BINARY_OPERATION(concat, string, string);
CHECK_BINARY_OPERATION(comma, void, string);
CHECK_TERNARY_OPERATION(ternary, number, string, string);
CHECK_INDEX_OPERATION(lstring);
CHECK_CALL_OPERATION(lstring);
default:
throw expression_builder_error();
}
}
如果是这种情况, 它将检查节点是否保持字符串常量, 并构建constant_expression。
然后, 它检查节点是否持有标识符, 并在这种情况下返回lstring类型的全局或局部变量表达式。如果我们实现常量变量, 它可以保存一个标识符。否则, 它假定节点保留节点操作, 并尝试所有可以返回字符串的操作。
这是CHECK_IDENTIFIER和CHECK_BINARY_OPERATION宏的实现:
#define CHECK_IDENTIFIER(T1)\
if (std::holds_alternative<identifier>(np->get_value())) {\
const identifier& id = std::get<identifier>(np->get_value());\
const identifier_info* info = context.find(id.name);\
if (info->is_global()) {\
return std::make_unique<\
global_variable_expression<R, T1>\
>(info->index());\
} else {\
return std::make_unique<\
local_variable_expression<R, T1>\
>(info->index());\
}\
}
#define CHECK_BINARY_OPERATION(name, T1, T2)\
case node_operation::name:\
return expression_ptr(\
std::make_unique<name##_expression<R, T1, T2> > (\
expression_builder<T1>::build_expression(\
np->get_children()[0], context\
), \
expression_builder<T2>::build_expression(\
np->get_children()[1], context\
)\
)\
);
CHECK_IDENTIFIER宏必须参考compile_context来构建具有适当索引的全局或局部变量表达式。这是在此结构中对editor_context的唯一用法。
你可以看到CHECK_BINARY_OPERATION递归调用子节点的build_expression。
包起来
在我的GitHub页面上, 你可以获得完整的源代码, 对其进行编译, 然后键入表达式并查看评估后的变量的结果。
我可以想象, 在人类创造力的各个分支中, 有一段时间作者在某种意义上意识到自己的产品还活着。在构建编程语言时, 这是你可以看到该语言”呼吸”的时刻。
在本系列的下一个也是最后一部分, 我们将实现其余的最小语言功能集, 以使其实时运行。
评论前必须登录!
注册