我正在开发一个包含小型DSL的项目。用这种语言对字符串进行词法分析和解析得到一个解析树,实现为一个名为Expr的抽象类,然后它有许多常用的派生类,如AssignmentExpr,InvokeExpr,AdditionExpr等等,对应于分配的解析树节点,函数调用,添加等。该项目以C#实现。
我目前正在考虑为此DSL实施类型推断。这意味着我希望能够获取Expr类的实例并返回有关树中不同节点类型的编码信息。此类型信息取决于符号表(变量类型)和函数表(函数签名)。因此,我想做一些事情:
TypedExpr typedExpr = inferTypes(expr, symbolTable, functionTable)
这里,TypedExpr理想情况下与Expr类似,只是Type属性给出了表达式的类型。然而,这会带来以下设计问题:
TypedExpr从Expr继承并简单地实现一个额外的属性Type是有意义的。但是,这将创建两个并行继承层次结构,一个用于TypedExpr(TypedAssignmentExpr,TypedInvokeExpr等),另一个用于Expr(AssignmentExpr,InvokeExpr等等)。这不便于维护,如果需要进一步扩展解析树,则问题会扩大。我不确定如何减轻这种情况。一种可能性是桥梁设计模式,但我不认为这能够完全解决问题。
或者,Expr可以简单地实现一个Type属性,该属性在构造时从解析器开始为空,之后由类型推断算法填充。但是,传递具有空字段的对象会引发NullReferenceExceptions。 TypedExpr的想法可以缓解这个问题。此外,鉴于Expr类的想法是表达一个解析树,类型信息实际上并不是树的一部分:输入是上下文敏感的,需要特定的符号和函数表。
第三,类型推断方法也可以简单地返回Dictionary< Expr,类型>它编码有关所有节点的类型信息。这意味着Expr仍然只代表解析树。这样做的缺点是构造的字典对象没有任何明显的属性,表明它是专门链接到传递给类型推断方法的Expr对象。
我对上面给出的三种解决方案中的任何一种都不满意。
我的问题是:针对这个问题的各种方法有哪些好处和缺点?类型信息应该直接在解析树中编码,还是应该使用并行树类?或者词典解决方案最好?是否有可接受的“最佳实践”解决方案?
答案 0 :(得分:1)
继续选择二。这可以被视为“最佳实践”。
原因是编译器通常在许多通道(阶段,阶段)中工作。解析是第一个,键入解决方案另一个。您可以稍后添加优化传递,代码生成传递等。通常,单个数据结构,抽象语法树(AST;或解析树)在这些传递中都是主要的。
“传递具有空字段的对象邀请NullReferenceExceptions”的想法只是错误的担忧。您必须处理无效案例,无论如何都要引入反措施来验证输入/输出。编译器(包括简单表达式处理器)是由复杂规则驱动的非常复杂的事情,它涉及高度的数据结构复杂性和您无法避免的应用程序逻辑。
AST具有未初始化数据非常正常。每个编译过程,除了解析器初始构建AST之外,然后操纵AST,计算更多信息(如类型解析阶段)。 AST甚至可能发生实质性变化,即由于优化过程。
旁注:现代编译器(如最新的C#编译器)对AST和其他内部数据结构采用非可变性策略。在这种情况下,每个传递构建自己的新数据结构。然后,您可以为每个传递设计一组新的数据结构,但这可能会变成一个过于复杂的代码来维护。来自C#编译器团队的人可以详细说明这个主题。