语法的二义性

JavaCC不能分析所有EBNF描述的语法,因为EBNF描述的语法本质上具有二义性的情况。
C语言中if语句用JavaCC的EBNF可以是如下描述:

"if" "(" expr() ")" stmt() ["else" stmt()]

作为符合上述规则的具体代码,可以由如下例子:

if (cond1)
if (cond2)
f();
else
g();

根据上面的规则分析下这段代码,直观的看上述代码表述的应该是这样的:

if (cond1) {
if (cond2) {
f();
} else {
g();
}
}

但是依据规则仔细分析下,下面的解析也是有可能的:

if (cond1) {
if (cond2) {
f();
}
} else {
g();
}

也就是说对于1份代码可以生成如下图这样的2颗语法树,像这样对于单个输入可能由多种解释时,这样的语法就可以说存在二义性。
[待截图]

JavaCC的局限性

除了上面说到的问题,JavaCC本身也存在局限性而无法正确解析程序。例如像下面这样描述语法时就会发生这种问题:

type() : {}
{
<SIGNED> <CHAR> // 选项1
| <SIGNED> <SHORT> // 选项2
| <SIGNED> <INT> // 选项3
| <SIGNED> <LONG> // 选项4
...

JavaCC在遇到用“|”分隔的选项时,在仅读取了1个token的时刻就会对选项进行判断,确切的动作如下:

  1. 读取1个token
  2. 按照书写顺序依次查找由上述token开头的选项
  3. 找到的话就选用该选项

也就是说,根据上述规则,JavaCC在读取了<SIGNED>token时就已经选择了<SIGNED><CHAR>,即选项1,因此即便写了选项2和选项3,无意义。这个问题称为JavaCC的选择冲突。

提取左侧共通部分

当你写了会发生选择冲突的规则情况下,若用JavaCC处理该语法描述文件就会给出警告信息。
如果消息中出现了Choice conflict字眼,就说明了发生了选择冲突。
解决上述问题的方法有两种,其一就是讲选项左侧共通的部分提取出来。以刚才的选择为例子,就改为如下这样:

type() : {}
{
<SIGNED> (<CHAR> | <SHORT> | <INT> | <LONG>)
}

这样就不会由选择冲突了。
如有通过这种方法仍无法解决的问题可以使用下面的token超前扫描来解决。

token的超前扫描

只要明确指定, JavaCC可以在读取更多的token后再决定选择哪个选项。这个功能就是token的超前扫描。

type(): {}
{
LOOKAHEAD(2) <SIGNED> <CHAR>
| LOOKAHEAD(2) <SIGNED> <SHORT>
| LOOKAHEAD(2) <SIGNED> <INT>
| <SIGNED> <LONG>
}

LOOKAHEAD就是"读取2个token后,如果读取的token和该选项符合则选择该选项"。
最后的选项不需要使用LOOKAHEAD, 因为 LOOKAHEAD是在还剩下多个选项时,为了延迟决定选择哪个选项而使用的功能。JavaCC会优先选用先描述的选项,因此当到达最后的选项时意味这其他选项都不符合,在只剩下一个选项时,即便推迟选择没有意义。

可以省略的规则和冲突

除了“选择”外,选择冲突在“可以省略”或“重复0次或多次”中也有可能发生。
可以省略的规则中会发生"是省略还是不省略"的冲突,之前的空悬else问题就是一个具体的例子。空悬else的问题在于内侧的if语句的else部分是否省略。如果内侧的if语句的else部分没有省略,则else部分属于内侧的if语句,如果省略的话则属于外侧的if语句。
空悬else最直观的判断方法是else属于最内侧的if,因此试着使用LOOKAHEAD来进行判断。未使用 LOOKAHEA的规则描述如下所示:

if_stmt() : {}
{
<IF> "(" expr ")" stmt() [<ELSE> stmt()]
}

使用LOOKAHEAD来避免冲突发生的规则如下:

if_stmt() : {}
{
<IF> "(" expr() ")" stmt() [LOOKAHEAD(1) <ELSE> stmt()]
}

) 则不省略 stmt()。 这样就能明确else始终属于最内侧的if语句。

重复与冲突

重复的情况下会发生"是作为重复的一部分还是跳出重复"这样的选择冲突。
下面是cflat中表示形参的声明规则。

param_decls() : {}
{
type() ("," type())* ["," "..."]
}

根据上述规则,在读取type()后又读到","时,本来可能是"," type()也可能是"," "...",但JavaCC默认只向前读取一个token,因此在读到","时就必须判断是继续重复还是跳出重复,并且恰巧","和("," type())的开头一致,所以JavaCC会一直判断为重复("," type())×,而规则"," "..."则完全不会被用到。实际上如果程序中出现"," "..." 会因为不符合规则"," type() 而判定语法错误。
解决方法如下:

param_decls(): {}
{
type() (LOOKAHEAD(2) "," type())* ["," "..." ]
}

更灵活的超前扫描

JavaCC提供了更灵活的超前扫描功能,可以指定"读取符合规则的所有token"。

definition() : {}
{
storage() type() <IDENTIFIER> ";"
| storage() type() <IDENTIFIER> arg_decls() block()
...
}

上述是cflat的参数定义和函数定义的规则。左侧的部分完全一样,这样的规则会发生选择冲突。
用超前扫描来分析上述规则,读取"恰好n个"token是行不通的。原因在于共通部分storage() type() <IDENTIFIER>中存在非终端符号storage()和type()。因为不知道storage()和type()实际对应几个token,所以无法用“恰好n个token”来处理。
这里就需要用"读取符合这个规则的所有token"这样的设置。上述规则中选择项的共通部分是storage() type() <IDENTIFIER>,因此只要读取了共通部分加上1个token即storage() type() <IDENTIFIER>,就能够区别2个选项了。规则改写如下:

definition(): {}
{
LOOKAHEAD(storage() type() <IDENTIFIER> ";")
storage() type() <IDENTIFIER> ";"
| storage() type() <IDENTIFIER> arg_decls() block()
...
}

只需在LOOKAHEAD的括号中写上需要超前扫描的规则即可。这样利用超前扫描能够顺利区分2个选项了。

语法的二义性和token的超前扫描的更多相关文章

  1. Compiler Theory(编译原理)、词法/语法/AST/中间代码优化在Webshell检测上的应用

    catalog . 引论 . 构建一个编译器的相关科学 . 程序设计语言基础 . 一个简单的语法制导翻译器 . 简单表达式的翻译器(源代码示例) . 词法分析 . 生成中间代码 . 词法分析器的实现 ...

  2. Lucene学习总结之八:Lucene的查询语法,JavaCC及QueryParser

    一.Lucene的查询语法 Lucene所支持的查询语法可见http://lucene.apache.org/java/3_0_1/queryparsersyntax.html (1) 语法关键字 + ...

  3. merge 语句的语法

    /*Merge into 详细介绍 MERGE语句是Oracle9i新增的语法,用来合并UPDATE和INSERT语句. 通过MERGE语句,根据一张表或子查询的连接条件对另外一张表进行查询, 连接条 ...

  4. 2018-02-04 AppleScript类自然语言与非英语语法设计

    最早知晓是之前C#中文版的github讨论里提到了AppleScript有多语言版. 昨天想起, 觉得它毕竟是为数不多(仅有的?)大公司开发的非英语语法的编程语言, 不禁好奇它的前世今生. 于是作了一 ...

  5. Lucene的查询语法,JavaCC及QueryParser(1)

    http://www.cnblogs.com/forfuture1978/archive/2010/05/08/1730200.html 一.Lucene的查询语法 Lucene所支持的查询语法可见h ...

  6. 几个很好用SQL语法(SqlServer)

    1,MERGE INTO 语句: 这个语法仅需要一次全表扫描就完成了全部工作,执行效率要高于INSERT+UPDATE,作用还是很强大的(简单的说就是它可以批量更新和插入处理一个数据集,如果存在就更新 ...

  7. ORACLE中的MERGE语法使用记录

    项目中使用到了Oracle的MERGE INTO语句,在这里简单记录下使用方法 使用场景如下: 存在对一张数据量很大的表,你需要对里面的大量数据进行更新,如果数据不存在,就进行插入的操作. 常规想到的 ...

  8. Linux服务器的弱口令检测及端口扫描

    一.弱口令检测--John the Ripper John the Ripper工具可以帮助我们扫描出系统中密码安全性较低的用户,并将扫描后的结果显示出来. 1.安装John the Ripper: ...

  9. Lucene学习总结之八:Lucene的查询语法,JavaCC及QueryParser 2014-06-25 14:25 722人阅读 评论(1) 收藏

    一.Lucene的查询语法 Lucene所支持的查询语法可见http://lucene.apache.org/java/3_0_1/queryparsersyntax.html (1) 语法关键字 + ...

随机推荐

  1. openstack 之~keystone之网关协议

    第一:静态页面和动态页面 上一篇博客介绍了HTTP后,我们知道一个web server的本质就是 浏览器发送一个HTTP请求: 服务器收到请求,生成一个HTML文档: 服务器把HTML文档作为HTTP ...

  2. 在 iOS 中信任手动安装的证书描述文件

    在 iOS 10.3 及更高版本中,手动安装包含证书有效负载的描述文件时,这个证书不会自动受 SSL 信任.   在安装通过电子邮件发送给您或从网站下载的描述文件时,您必须手动开启受 SSL 信任. ...

  3. shell字符串的用法

    shell字符串的用法 注意:shell4.2和shell4.1会有差别,较低版本的shell可能不支持某些功能 获取字符串长度:${#string} 获取子串: 注:(左边的第一个字符是用 0 表示 ...

  4. Maven中classifier

    1.classifier概述 classifier通常用于区分从同一POM构建的具有不同内容的构件(artifact).它是可选的,它可以是任意的字符串,附加在版本号之后. 2.使用场景 场景一:区分 ...

  5. linux平台下Tomcat的安装与优化

    Tomcat 服务器是一个免费的开放源代码的Web 应用服务器,属于轻量级应用服务器,在中小型系统和并发访问用户不是很多的场合下被普遍使用,是开发和调试JSP 程序的首选.对于一个初学者来说,可以这样 ...

  6. 微信小程序判断用户是否需要再次授权获取个人信息

    一.index.js设置如下 //获取用户的授权信息,放到本地缓存中 wx.getSetting({ success: (res) => { if(res.authSetting['scope. ...

  7. Python实现多进程

    Python可以实现多线程,但是因为Global Interpreter Lock (GIL),Python的多线程只能使用一个CPU内核,即一个时间只有一个线程在运行,多线程只是不同线程之间的切换, ...

  8. failed to create process怎么解决

    python 在cmd时,报错:failed to create process怎么解决 在cmd命令前加 : python -m 命令(如:python -m conda update conda)

  9. 【centos6.6环境搭建】Github unable to access SSL connect error出错处理

    问题 克隆github项目出现SSL connect error git clone https://github.com/creationix/nvm Cloning into 'nvm'... f ...

  10. Linux输入子系统框架分析(1)

    在Linux下的输入设备键盘.触摸屏.鼠标等都能够用输入子系统来实现驱动.输入子系统分为三层,核心层和设备驱动层.事件层.核心层和事件层由Linux输入子系统本身实现,设备驱动层由我们实现.我们在设备 ...