JavaCC:词法和语法分析器生成器

前言

现需要实现一个数据加工的DSL(domain-specific language 领域特定语言),不需要对DSL过多理解,你可以类比SQL语言即可。现使用JavaCC这个工具来生成一个词法和语法解析器,注意是代码生成器,所以才叫CC(Compiler Compiler编译器的编译器)。

技术理念

DSL

domain-specific language 领域特定语言,最典型的就是SQL,还有比如lucene Query Parser SyntaxpromQL等。DSL主要分为两类。
内部DSL)内嵌到现有的编程语言(java,c,python)上。比如lucene,PromQL等。
外部DSL)拥有自己一套完整的词法分析,语法分析,编译,代码生成等一整套完整的体系的独立语言,比如JSON,正则,XML,markdown等。

javaCC

想个很简单的问题,计算机如何识别你表达的语言在按你的语言去执行相应的逻辑?这就是编译原理,将你的输入的内容也叫代码语言通过词法语法分析、语法翻译、中间代码生成、存储管理、代码优化和目标代码等过程翻译成计算机能识别的代码。那么同理DSL也需要一个这样的过程。业内有很多优秀的中间件的词法和语法解析器代码生成器都是javaCC。比如lucene QueryParser.jj,Calcite parser.jj,JsqlParser JSqlParserCC.jjt等,更多可以直接看官网。

编译器原理

dsl-编译器流程.drawio.png

模块说明
词法分析:将输入的文本代码解析生成Token流,也叫扫描器。
语法分析:利用词法分析生成的Token流转成AST(抽象语法树),也叫解析器。
语义分析:分析语法树,得到新的语法树。
中间代码:分析语法树,生成中间代码。
而JavaCC则主要担当生成词法分析器和语法分析器。

javaCC解析器原理

javaCC的工作即为生成词法分析器(lexical analyzer)和语法分析器(parser generators)的工具。真正对文本代码分析的流程入图。


dsl-javaCC原理.drawio.png

一些信息
1,javaCC 使用自顶向下(top-down)递归下降(recursive descent)解析。
2,默认是LL(1)文法用于预测性解析,不过在使用 LOOKAHEAD(n) 的时候为LL(k),主要为了消除歧义,而在其他情况下均为LL(1),这样能减少递归回溯。至于啥是LL(k)算法,去看编译原理吧,我也看得一知半解。
3,javaCC词法分析器使用正则或字符串定义,语法解析器定义使用EBNF(扩展巴科斯范式)语法,使用EBNF可以减少左递归的影响且更加易读,词法分析器和语法分析器使用同一个文件定义。
4,javaCC提供了自带的JJTree,非常方便直接生成AST树。

本地环境安装

安装官方参考文档。官方使用的是linux安装,我这边使用的是win本地开发。
1,下载安装包(javacc-7.0.10.tar.gz),下载地址
2,解压到相应的目录并配置环境变量

#解压安装路径
F:\soft3\javacc-javacc-7.0.10
 
#环境变量
JAVACC_HOME : F:\soft3\javacc-javacc-7.0.10\scripts
 
#配置path
%JAVACC_HOME%

3,需要特别注意的是,需要在安装目录的根目录下创建一个target文件夹,然后将bootstrap文件夹中的javacc.jar拷贝过去,否则会报错。


image.png

4,检查是否安装成功
打开cmd,输入javacc,出现如下信息则表示安装成功。


image.png

写个Demo

在开始开发javacc的解释器前,需要了解一些基本知识,否则会非常绕不方便理解,我这边的例子参考官方给的javacc-tutorial个人感觉是写的最清楚的英文也比较简单,网上写的都让人云里雾里,不过可惜的是一些图表信息不完整,jjtree说的也不详细,不过对于我们入门学习还是非常足够了)。

BNF语法

::=     //“定义为”, ::== 符号表示为 “可扩展为” 或 “可替换为”, “可定义为”.
<A>     //A为必选项
“A”   //A是一个术语,不用翻译
'A'     //A是一个术语,不用翻译
[A]     //A是可选项
{A}     //A是重复项,可出现任意次数,包括0次
A*      //A是重复项,可出现任意次数,包括0次
A+      //A可出现1次或多次
(A B)   //A和B被组合在一起
A|B     //A、B是并列选项,只能选一个

javaCC xx.jj文件模板

options {
  JavaCC 的选项
}
 
PARSER_BEGIN(解析器类名)
package 包名
import 库名
public class 解析器类名 {
  任意的 Java 代码
}
PARSER_END(解析器类名)
 
扫描器的描述
 
解析器的描述

一些说明
1,options部分,用于放置 JavaCC 的选项。常用选项见下文中的 JavaCC 语法描述文件中的 options。
2,PARSER_BEGIN、PARSER_END部分,用于定义解析器类。解析器类中需要定义的成员和方法都写在这里。
3,扫描器的描述部分,用于定义扫描器。
4,解析器的描述部分,用于定义解析器。

生成词法和语法解析器文件说明

XXConstants: 定义一些常量值,比如将TOKEN定义的值转换为一个个的数字;
XXParserTokenManager: token管理器, 用于读取token, 可以自定义处理;
JavaCharStream: CharStream的实现,会根据配置选项生成不同的类;
ParseException: 解析错误时抛出的类;
Token: 读取到的单词描述类;
TokenMgrError: 读取token错误时抛出的错误;

xx.jj 一些关键字

TOKEN                       /* 定义一些确定的普通词或关键词,主要用于被引用 */
SPECIAL_TOKEN               /* 定义一些确定的特殊用途的普通词或关键词,主要用于被引用或抛弃 */
SKIP                        /* 定义一些需要跳过或者忽略的单词或短语,主要用于分词或者注释 */
MORE                        /* token的辅助定义工具,用于确定连续的多个token */
EOF                         /* 文件结束标识或者语句结束标识 */
IGNORE_CASE                 /* 辅助选项,忽略大小写 */
JAVACODE                    /* 辅助选项,用于标识本段代码是java */
LOOKAHEAD                   /* 语法二义性处理工具,用于预读多个token,以便明确语义 */
PARSER_BEGIN                /* 样板代码,固定开头 */
PARSER_END                  /* 样板代码,固定结尾 */
TOKEN_MGR_DECLS             /* 辅助选项 */

实现javacc-tutorial中的计算器例子

1,编写calculator0.jj文件

# javacc全局配置,更多的options可以查看https://javacc.github.io/javacc/documentation/grammar.html#options
options {
  STATIC = false ;
}
 
# 开始java代码块,内部的实现主要是实现main函数,完成解析的调用和执行
PARSER_BEGIN(Calculator)
    public class Calculator {
    static public void main(String[] args) throws ParseException, TokenMgrError, NumberFormatException {
      Calculator parser = new Calculator(System.in);
      parser.Start(System.out) ;
    }
  double previousValue = 0.0 ;
}
# java块结束
PARSER_END(Calculator)
 
# 词语解析器开始,使用正则表达式定义
 
 
## SKIP 表示忽略那些字符的扫描
SKIP : { " " }
 
 
## 表示扫描的结束符
TOKEN : { < EOL : "\n" | "\r" | "\r\n" > }
 
 
## 加减乘除关键词定义
TOKEN : { < PLUS : "+" > }
TOKEN : { < MINUS : "-" > }
TOKEN : { < TIMES : "*" > }
TOKEN : { < DIVIDE : "/" > }
 
 
## 读取值
TOKEN : { < PREVIOUS : "$" > }
 
 
## 数值定义,包括整形和浮点数,注意使用DIGITS替换整个(["0"-"9"])+的重复定义,增加可读性,减少重复部分。
TOKEN : { < NUMBER : <DIGITS> | <DIGITS> "." <DIGITS> | <DIGITS> "." | "."<DIGITS> > }
 
 
## 数值定义,由于被其他Token定义引用,因此名称使用#表示
TOKEN : { < #DIGITS : (["0"-"9"])+ > }
 
 
## 左右括号
TOKEN : { < OPEN_PAR : "(" > }
TOKEN : { < CLOSE_PAR : ")" > }
 
# 解析器开始函数,内部实现为计算表达式结果后,控制台打印结果。
void Start(PrintStream printStream) throws NumberFormatException :
{}
{
  (
    previousValue = Expression()
    <EOL>
    { printStream.println( previousValue ); }
  )*
  <EOF>
}
 
# 表达式解析,可以看到函数基本上和java语法差不多,不过函数内部实现为EBNF语法,这样看起来比较麻烦,可以可以一层一层看下去,Primary()将Token.image返回扫描到表达式中数值转换成double返回,Term()控制乘法和除法优先级, Expression()计算整个表达式的结果。
# 这样函数看起来不够清晰,我举一个简单的例子 <NUMBER>(<PLUS><NUMBER>)*<EOF> 这明显就是一个EBNF表达式,表示第一个字符串必须为数值,而()中表示连接,第二个字符必须为加号,第三个必须为数值,然后*表示0到无数次。而抽出多个函数,主要是为了可读性和优先级以及特殊# # 处理。
double Expression() throws NumberFormatException :
{
  double i ;
  double value ;
}
{
  value = Term()
  (
    <PLUS>
    i = Term()
    ### 大括号中的表示动作
    { value += i ; }
  |
    <MINUS>
    i = Term()
    { value -= i ; } 
     
  )*
  { return value ; }
}
 
#对乘法和除法优先级处理
double Term() throws NumberFormatException :
{
  double i ;
  double value ;
}
{
  value = Primary()
  (
    <TIMES>
    i = Primary()
    { value *= i ; }
    |
    <DIVIDE>
    i = Primary()
    { value /= i ; }
    )*
  { return value ; }
}
 
#如果是数值,直接解析为Double数据,如果为$则返回计算值,如果使用()括号包括则递归调用计算表达式,如果第一个字符为-负号,则取负值。
double Primary() throws NumberFormatException :
{
  Token t ;
  double d ;
}
{
  t=<NUMBER>
  { return Double.parseDouble( t.image ) ; }
  |
  <PREVIOUS>
  { return previousValue ; }
  |
  <OPEN_PAR> d=Expression() <CLOSE_PAR>
  { return d ; }
  |
  <MINUS> d=Primary()
  { return -d ; }
}

2,生成词法和语法解析器

#进入项目
cd ..\calculator2

#执行
javacc calculator0.jj
image.png

image.png

看到如此信息则表示创建词法和语法解析成功。

3,编译java文件

javac *.java
image.png

image.png

4,创建表达式文件input.txt,注意要有一个换行的结束符。


image.png

5,执行结果

java Calculator <input.txt
image.png

总结

1,javaCC是java语言中最强大且使用最广的词法和语法解析器代码生成器。
2,当前demo代码是直接在xx.jj解析器动作中直接计算的,通常情况下一般不会这样,而是先试用jjtree转换成AST抽象语法树后再做后续的逻辑。
3,javaCC主要是生成词法和语法解析器,将文本代码转换成计算机可读可理解的AST抽象语法树。
4,javacc使用正则表达式描述词法规则,使用EBNF表示法描述语法规则。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 214,588评论 6 496
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 91,456评论 3 389
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 160,146评论 0 350
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 57,387评论 1 288
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 66,481评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 50,510评论 1 293
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,522评论 3 414
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,296评论 0 270
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,745评论 1 307
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,039评论 2 330
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,202评论 1 343
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,901评论 5 338
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,538评论 3 322
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,165评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,415评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,081评论 2 365
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,085评论 2 352

推荐阅读更多精彩内容