Methods 编译器/解释器设计:内置方法应该有自己的节点还是应该使用查找表?

Methods 编译器/解释器设计:内置方法应该有自己的节点还是应该使用查找表?,methods,compiler-construction,lookup,abstract-syntax-tree,interpreter,Methods,Compiler Construction,Lookup,Abstract Syntax Tree,Interpreter,我正在设计一个使用递归下降的解释器,我已经开始实现内置方法 我正在实现的方法的一个示例是输出到控制台的print方法,就像python的print方法和Java的System.out.println一样 然而,我注意到有多种方法可以实现这些内置方法。我相信还有很多,但我已经确定了两种可行的方法来实现这一点,我正在努力确定哪种方法是最好的做法。下面的上下文是我在解释器中使用的不同层,它松散地基于和我遇到的其他教程 雷克瑟 分析器 语义分析器 解释器/代码生成器 一,。为每个单独的内置方法创建AST

我正在设计一个使用递归下降的解释器,我已经开始实现内置方法

我正在实现的方法的一个示例是输出到控制台的print方法,就像python的print方法和Java的System.out.println一样

然而,我注意到有多种方法可以实现这些内置方法。我相信还有很多,但我已经确定了两种可行的方法来实现这一点,我正在努力确定哪种方法是最好的做法。下面的上下文是我在解释器中使用的不同层,它松散地基于和我遇到的其他教程

雷克瑟 分析器 语义分析器 解释器/代码生成器 一,。为每个单独的内置方法创建AST节点

该方法需要对解析器进行编程,以便为每个方法生成一个节点。这意味着每个方法将存在一个唯一的节点。例如:

当在lexer中找到TPRINT令牌时,解析器将查找生成节点

print : TPRINT TLPAREN expr TRPAREN {$$ = new Print($3);}
      ;
这就是print类的外观

class Print : public Node {
public:
    virtual VariableValue visit_Semantic(SemanticAnalyzer* analyzer) override;
    virtual VariableValue visit_Interpreter(Interpreter* interpreter) override;
    Node* value;
    Print(Node* cvalue) {
        value = cvalue;
    }
}

在此基础上,我定义了visit_语义和visit_解释器方法,并从顶部节点使用递归访问它们

我可以想到使用这种方法的几个优点/缺点:

优势

当代码生成器遍历树并访问Print节点的visit_解释器方法时,它可以直接执行响应,因为它被编程到它的visit方法中。 缺点

我将不得不写很多复制粘贴代码。我必须为每个单独的方法创建一个节点,并定义它的解析器语法。 二,。为方法调用节点创建通用AST节点,然后使用查找表确定要调用的方法

这涉及到创建一个泛型节点MethodCall和语法来确定是否调用了一个方法,该方法具有一些唯一标识符,例如它所引用的方法的字符串。然后,当调用MethodCall的visit_解释器或visit_语义方法时,它会在表中查找要执行的代码

methcall : TIDENTIFIER TLPAREN call_params TRPAREN {$$ = new MethodCall($1->c_str(), $3);}
         ;
方法调用节点。这里唯一的标识符是std::string methodName:

优点:

一个通用语法/节点用于所有方法调用。这使它更具可读性 缺点:

在某些情况下,必须在查找表中比较唯一标识符std::string methodName,以确定其响应。这不如在响应节点的访问方法时直接编程有效。 在编译器/解释器中,哪种做法是处理方法的最佳方式?是否有更好的不同做法,或者是否有我遗漏的其他缺点/优点


我对编译器/解释器设计还比较陌生,所以请让我澄清一下我是否有一些术语错误。

您应该使用查表。它让你的事情变得容易多了。另外,考虑一下用户定义的功能!那么您肯定需要一个表。

您应该使用表查找。它让你的事情变得容易多了。另外,考虑一下用户定义的功能!那么您肯定需要一个表。

在我看来,您必须在某个地方将内容拆分为方法。问题是,您想将此作为解析器定义解决方案1的一部分来实现,还是想在C++侧解决方案2上实现这个?< /P> P>个人,我更希望保持解析器定义简单,并将此逻辑移到C++侧,即解决方案2。 从解决方案2的运行时角度来看,我不会太担心这一点。但最终,这取决于调用该方法的频率以及您拥有多少标识符。只有几个标识符不同于以else-if方式比较数百个字符串

您可以首先以简单的直接方式实现它,即以else-if方式匹配字符串标识符,并查看是否遇到运行时问题


如果遇到运行时问题,可以使用哈希函数。核心方法是自己实现一个最优哈希函数,并离线检查哈希函数的最优性,因为您知道字符串标识符的集合。但对于您的应用程序来说,这可能有点过头了,或者出于学术目的,我建议您只使用STL中的无序映射,它在后台使用散列,请参见将字符串标识符映射到索引号,因此,您可以通过对这些索引号执行有效的切换操作来实现跳转表。

在我看来,您必须在某个地方将内容拆分为方法。问题是,您想将此作为解析器定义解决方案1的一部分来实现,还是想在C++侧解决方案2上实现这个?< /P> 就我个人而言, 我更希望保持解析器定义简单,并将此逻辑移到C++侧,即解决方案2。 从解决方案2的运行时角度来看,我不会太担心这一点。但最终,这取决于调用该方法的频率以及您拥有多少标识符。只有几个标识符不同于以else-if方式比较数百个字符串

您可以首先以简单的直接方式实现它,即以else-if方式匹配字符串标识符,并查看是否遇到运行时问题


如果遇到运行时问题,可以使用哈希函数。核心方法是自己实现一个最优哈希函数,并离线检查哈希函数的最优性,因为您知道字符串标识符的集合。但对于您的应用程序来说,这可能有点过头了,或者出于学术目的,我建议您只使用STL中的无序映射,它在后台使用散列,请参见将字符串标识符映射到索引号,这样,您就可以通过对这些索引号执行有效的切换操作来实现跳转表。

对于用户定义的函数,您仍然需要查找表,因此将其用于内置函数也是有意义的。您希望将print之类的东西设置为关键字的唯一原因是,您希望它允许特殊语法,而这种语法不是常规函数调用语法的一部分。Python2有一个print关键字,但请注意,Python3不再使用这个关键字,而是将print作为一个常规函数。这是一个非常好的观点,感谢您对于用户定义的函数仍然需要查找表,因此将其用于内置函数也是有意义的。您希望将print之类的东西设置为关键字的唯一原因是,您希望它允许特殊语法,而这种语法不是常规函数调用语法的一部分。Python2有一个print关键字,但请注意Python3不再使用这个关键字,而是将print作为一个常规函数
class MethodCall : public Node {
public:
    virtual VariableValue visit_Semantic(SemanticAnalyzer* analyzer) override;
    virtual VariableValue visit_Interpreter(Interpreter* interpreter) override;
    std::string methodName;
    ExprList *params;
    MethodCall(std::string cmethodName, ExprList *cparams) {
        params = cparams;
        methodName = cmethodName;
    }
};