上一篇博客讲到了构造语法树的问题。有朋友在留言问我,为什么一定要让语法分析器产生语法树,而不是让用户自己决定要怎么办呢?在这里我先解答这个问题。

1、大部分情况下都是真的需要有语法树
2、如果要直接返回计算结果之类的事情的话,只需要写一个visitor运行一下语法树就好了,除去自动生成的代码以外(反正这不用人写,不计入代价),代码量基本上没什么区别
3、加入语法树可以让文法本身描述起来更简单,如果要让程序员把文法单独放在一边,然后自己写完整的语义函数来让他生成语法树的话,会让大部分情况(需要语法树)变得特别复杂,而少数情况(不需要语法树)又没有获得什么好处。

尽管类似yacc这样的东西的确是不包含语法树的内容而要你自己写的,但是用起来难道不是很难受吗?

现在转入正题。这一篇文章讲的主要是构造符号表的问题。想要把符号表构造的好是一件很麻烦的问题。我曾经尝试过很多种方法,包括强类型的符号表,弱类型的符号表,基于map的符号表等等,最后还是挑选了跟Visual Studio自带的用来读pdb文件的DIA类其中的IDIASymbol(http://msdn.microsoft.com/en-us/library/w0edf0x4.aspx)基本上一样的结构:所有的符号都只有这么一个symbol类,然后包罗万象,什么都有。为什么最后选择这么做呢?因为在做语义分析的时候,其实做的最多的事情不是构造符号表,而是查询符号表。如果符号表是强类型的画,譬如说类型要一个类,变量要一个类,函数要一个类之类的,总是需要到处cast来cast去,也找不到什么好方法来在完成相同事情的情况下,保留强类型而不在代码里面出现cast。为什么语法树就要用visitor来解决这个问题,而符号表就不行呢?因为通常我们在处理语法树的时候都是递归的形式,而符号表并不是。在一个上下文里面,实际上我们是知道那个symbol对象究竟是什么东西的(譬如说我们查询了一个变量的type,那这返回值肯定只能是type了)。这个时候我们要cast才能用,本身也只是浪费表情而已。这个时候,visitor模式就不是和面对这种情况了。如果硬要用visitor模式来写,会导致语义分析的代码分散得过于离谱导致可读性几乎就丧失了。这是一个辩证的问题,大家可以好好体会体会。

说了这么一大段,实际上就是怎么样呢?让我们来看“文法规则”本身的符号表吧。既然这个新的可配置语法分析器也是通过parse一个文本形式的文法规则来生成parser,那实际上就跟编译器一样要经历那么多阶段,其中肯定有符号表:

class ParsingSymbol : public Object
{
public:enum SymbolType{Global,EnumType,ClassType,        // descriptor == base typeArrayType,        // descriptor == element type
        TokenType,EnumItem,        // descriptor == parentClassField,        // descriptor == field typeTokenDef,        // descriptor == token typeRuleDef,        // descriptor == rule type
    };
public:~ParsingSymbol();ParsingSymbolManager*            GetManager();SymbolType                        GetType();const WString&                    GetName();vint                            GetSubSymbolCount();ParsingSymbol*                    GetSubSymbol(vint index);ParsingSymbol*                    GetSubSymbolByName(const WString& name);ParsingSymbol*                    GetDescriptorSymbol();ParsingSymbol*                    GetParentSymbol();bool                            IsType();ParsingSymbol*                    SearchClassSubSymbol(const WString& name);ParsingSymbol*                    SearchCommonBaseClass(ParsingSymbol* classType);
};

因为文法规则本身的东西也不多,所以这里的symbol只能是上面记载的9种对象。这些对象其实特别的相似,所以我们可以看出唯一的区别就是当GetType返回值不一样的时候,GetDescriptorSymbol返回的对象的意思也不一样。而这个区别记载在了enum SymbolType的注释里面。实际上这种做法在面对超级复杂的符号表(考虑一下C++编译器)的时候并不太好。那好的做法究竟是什么呢?看上面IDIASymbol的链接,那就是答案。

不可否认,微软设计出来的API大部分还是很有道理的,除了Win32的原生GUI部分。

我们还可以看到,这个ParsingSymbol类的几乎所有成员函数都是用来查询这个Symbol的内容的。这里还有两个特殊的函数,就是SearchClassSubSymbol和SearchCommonBaseClass——当且仅当symbol是ClassType的时候才起作用。为什么有了GetSubSymbolByName,还要这两个api呢?因为我们在搜索一个类的成员的时候,是要搜索他的父类的。而一个类的父类的sub symbol并不是类自己的sub symbol,所以就有了这两个api。所谓的sub symbol的意思现在也很明了了。enum类型里面的值就是enum的sub symbol,成员变量是类的sub symbol,总之只要是声明在一个符号内部的符号都是这个符号的sub symbol。这就是所有符号都有的共性。

当然,有了ParsingSymbol,还要有他的manager才可以完成整个符号表的操作:

class ParsingSymbolManager : public Object
{
public:ParsingSymbolManager();~ParsingSymbolManager();ParsingSymbol*                    GetGlobal();ParsingSymbol*                    GetTokenType();ParsingSymbol*                    GetArrayType(ParsingSymbol* elementType);ParsingSymbol*                    AddClass(const WString& name, ParsingSymbol* baseType, ParsingSymbol* parentType=0);ParsingSymbol*                    AddField(const WString& name, ParsingSymbol* classType, ParsingSymbol* fieldType);ParsingSymbol*                    AddEnum(const WString& name, ParsingSymbol* parentType=0);ParsingSymbol*                    AddEnumItem(const WString& name, ParsingSymbol* enumType);ParsingSymbol*                    AddTokenDefinition(const WString& name);ParsingSymbol*                    AddRuleDefinition(const WString& name, ParsingSymbol* ruleType);ParsingSymbol*                    CacheGetType(definitions::ParsingDefinitionType* type, ParsingSymbol* scope);bool                            CacheSetType(definitions::ParsingDefinitionType* type, ParsingSymbol* scope, ParsingSymbol* symbol);ParsingSymbol*                    CacheGetSymbol(definitions::ParsingDefinitionGrammar* grammar);bool                            CacheSetSymbol(definitions::ParsingDefinitionGrammar* grammar, ParsingSymbol* symbol);ParsingSymbol*                    CacheGetType(definitions::ParsingDefinitionGrammar* grammar);bool                            CacheSetType(definitions::ParsingDefinitionGrammar* grammar, ParsingSymbol* type);
};

这个ParsingSymbolManager有着符号表管理器的以下两个典型作用

1、创建符号。
2、讲符号与语法树的对象绑定起来。譬如说我们在一个context下面推导了一个expression的类型,那下次对于同样的context同样的expression就不需要再推导一次了(语义分析有很多个pass,对同一个expression求类型的操作经常会重复很多次),把它cache下来就可以了。
3、搜索符号。具体到这个符号表,这个功能被做进了ParsingSymbol里面。
4、保存根节点。GetGlobal函数就是干这个作用的。所有的根符号都属于global符号的sub symbol。

因此我们可以怎么使用他呢?首先看上面的Add函数。这些函数不仅会帮你在一个符号表里面添加一个sub symbol,还会替你做一些检查,譬如说阻止你添加相同名字的sub symbol之类的。语法树很复杂的时候,很多时候我们有很多不同的方法来给一个符号添加子符号,譬如说C#的成员变量和成员函数。成员变量不能同名,成员函数可以,但是成员函数和成员变量却不能同名。这个时候我们就需要把这些添加操作封装起来,这样才可以在处理语法树(声明一个函数的方法可以有很多,所以添加函数符号的地方也可以有很多)的时候不需要重复写验证逻辑。

其次就是Cache函数。其实Cache函数这么写,不是用来直接调用的。举个例子,在分析一个文法的时候,我们需要把一个“类型”语法树转成一个“类型”符号(譬如说要决定一个文法要create什么类型的语法树节点的时候)。因此就有了下面的函数:

ParsingSymbol* FindType(Ptr<definitions::ParsingDefinitionType> type, ParsingSymbolManager* manager, ParsingSymbol* scope, collections::List<Ptr<ParsingError>>& errors)
{ParsingSymbol* result=manager->CacheGetType(type.Obj(), scope);if(!result){FindTypeVisitor visitor(manager, (scope?scope:manager->GetGlobal()), errors);type->Accept(&visitor);result=visitor.result;manager->CacheSetType(type.Obj(), scope, result);}return result;
}

很明显,这个函数做的事情就是,查询一个ParsingDefinitionType节点有没有被查询过,如果有直接用cache,没有的话再从头计算他然后cache起来。因此这些Cache函数就是给类似FindType的函数用的,而语义分析的代码则直接使用FindType,而不是Cache函数,来获取一个类型的符号。聪明的朋友们可以看出来,这种写法蕴含着一个条件,就是语法树创建完就不会改了(废话,当然不会改!)。

这一篇的内容就讲到这里了。现在的进度是正在写文法生成状态机的算法。下一篇文章应该讲的就是状态机究竟是怎么运作的了。文法所需要的状态机叫做下推状态机(push down automaton),跟regex用的NFA和DFA不太一样,理解起来略有难度。所以我想需要用单独的一篇文章来通俗的讲一讲。

转载于:https://www.cnblogs.com/geniusvczh/archive/2012/11/29/2793905.html

可配置语法分析器开发纪事(二)——构造符号表相关推荐

  1. 编译原理:LL1(1)文法的语法分析器(通过文法构造分析表)

    基本思想:( $ 表示空,即ε) (1)first集的算法思想 如果产生式右部第一个字符为终结符,则将其计入左部first集 如果产生式右部第一个字符为非终结符执行以下步骤 求该非终结符的first集 ...

  2. Vczh Library++ 语法分析器开发指南

    Vczh Library++ 语法分析器开发指南 陈梓瀚 前言 在日常的开发工作中我们总是时不时需要写一些语法分析器.语法分析器不一定指的是一门语言的编译器前端,也有可能仅仅是一个自己设计格式的配置文 ...

  3. 【转】Vczh Library++3.0之可配置语法分析器(前言)

    从网上无意间看到这个系列的文章,作者非常有想法,转下来慢慢研究,好好学习. 祝大家学习愉快,做自己的爱好 ^_^ ! 花了差不多两个星期的时间将一个可配置语法分析器(Combinator)写好了.这个 ...

  4. 【转】Vczh Library++ 3.0之可配置语法分析器(设计文法表达式)

    从网上无意间看到这个系列的文章,作者非常有想法,转下来慢慢研究,好好学习. 祝大家学习愉快,做自己的爱好 ^_^ ! 上一篇文章中我们看到了可配置语法分析器使用起来的样子,在这篇文章中我将告诉大家如何 ...

  5. C++轻量级可配置语法分析器(开源) - λ-calculus(惊愕到手了欧耶,GetBlogPostIds.aspx) - C++博客...

    C++轻量级可配置语法分析器(开源) - λ-calculus(惊愕到手了欧耶,GetBlogPostIds.aspx) - C++博客 C++轻量级可配置语法分析器(开源) - λ-calculus ...

  6. 从零写一个编译器(八):语义分析之构造符号表

    项目的完整代码在 C2j-Compiler 前言 在之前完成了描述符号表的数据结构,现在就可以正式构造符号表了.符号表的创建自然是要根据语法分析过程中走的,所以符号表的创建就在LRStateTable ...

  7. 基于Predictive Parsing的ABNF语法分析器(十二)——alternation、concatenation、group和option

    今天一鼓作气再写多点东西吧,这个题目差不多接近尾声了.来看看alternation.concatenation.group和option的解析代码: /*This file is one of the ...

  8. 微信小程序商城开发记录二之数据表结构设计

    文章目录 前言 1.用户表 2.产品表 3.商品品牌表 4.商品分类表 5.订单表 6.订单商品信息表 7.购物车表 8.收获地址表 9.省市区地址联动表 10.广告信息表 11.优惠券表 12.优惠 ...

  9. Linux 驱动开发之内核模块开发(四)—— 符号表的导出

    Linux内核头文件提供了一个方便的方法用来管理符号的对模块外部的可见性,因此减少了命名空间的污染(命名空间的名称可能会与内核其他地方定义的名称冲突),并且适当信息隐藏. 如果你的模块需要输出符号给其 ...

  10. 自制编译器:语法分析器(一)

    感觉语法分析器在编译器前端是一个较为庞大的东西,因此打算分两篇博客来描述,第一篇着重描述思想,第二篇具体论述实现. 1.语法分析器要做什么 在编写任何一个东西的的时候,都要先弄明白这个玩意儿是做什么的 ...

最新文章

  1. 9.69最长公共子串
  2. mmcv 对比 cv2 处理视频速度
  3. 点、圆和线的转化关系
  4. 6-汇编语言中段的使用+dw+start标号
  5. unity Scene窗口的任意比例放大和缩小
  6. yuzu模拟器安装设置大全
  7. 【Excel2019(六):数据透视表】【创建数据透视表+更改数据透视表汇总方式+数据透视表中的组合+汇总多列数据+创建计算字段+生成多张工作表】
  8. Python123 英文字符的鲁棒输入
  9. 连接服务器切换无线,怎么用路由器连接别人的wifi?
  10. Spring Boot 定制个性 banner
  11. MySQL中全局变量、会话变量、用户变量和局部变量的区别
  12. 嵌入式Linux--MYS-6ULX-IOT--构建交叉编译环境
  13. java modifier access_Java Modifier.getAccessSpecifier方法代码示例
  14. 中国大学mooc c语言作业,中国大学mooc程序设计入门——C语言:第三周测验(示例代码)...
  15. Odoo(OpenERP)应用实践:代发货管理
  16. M301H,M301A,CM201系列盒子刷机
  17. 感恩节---Thanksgiving Day
  18. Educational Codeforces Round 112 (Rated for Div. 2)(补题)
  19. Linux 生产环境搭建
  20. matlab图像雅可比行列式,函数矩阵与行列式(雅可比(Jacobi)矩阵与行列式)雅克...-雅可比矩阵-数学-詹底巧同学...

热门文章

  1. c语言 graph,基于图(graph)的应用举例
  2. 如何制作通讯录vcf_【教程】刷机或更换手机后快速导入通讯录的方法
  3. Spring身份验证+CXF拦截器+RESTful
  4. 【渝粤教育】电大中专电商运营实操 (16)作业 题库
  5. 【渝粤题库】 陕西师范大学 202331 证券投资学 作业(专升本)
  6. 【CoRL 2018】通过元策略优化的MBRL算法
  7. FPGA智能传感系统(二)基于FPGA的交通灯设计
  8. ubuntu16.04中 启动 Error starting userland proxy: listen tcp 0.0.0.0:5900: bind: address already in use
  9. CMake快速入门教程-实战
  10. 软件工程期末考试复习(二)