阅读更多

2顶
1踩

编程语言

转载新闻 如何撸一个领域语言

2016-06-03 11:43 by 副主编 mengyidan1988 评论(2) 有5833人浏览
引用

本文来自:云栖社区
作者:xiaoqb

DSL概述
DSL是一种抽象的概念,泛指用在特定领域的语言。例如在数据库管理系统中,使用SQL增删改查数据库内容,在C++编译中,Makefile也是一种DSL,它专用来描述各个编译单元的依赖关系以及编译参数,以此规则控制编译器和链接器。

从实现方法上来分类,DSL分为内部和外部DSL。内部与外部,指的是实现DSL的方式是否与宿主语言隔离。就是所谓内部DSL,就是用一种提供语言扩展功能的宿主语言来扩充的。比如Clojure支持暴露语法树给程序开发者(quote/unquote),用户可以以此来扩充原有语言的功能。而外部DSL则与源语言无关,重头写词法分析器、语法分析器以及解释执行器。

DSL的一大特点就是可以提高效率。从设计层面上来说,可以隔离两个系统之间的代码依赖,将其转变为可配置的数据,因此能够快速响应业务需求,对原系统的改动也比较小。从开发角度来说,DSL使得程序员可以专注某个领域的逻辑,从而不必关心相关原理与细节。这样对程序员来说,学习成本变低,代码量变少,程序出错的可能性也降低。例如,操作数据库只需要学会SQL语言就可以了,而并不需要掌握数据库底层的实现原理。

本文的应用场景在一些主流语言(Java,JavaScript,C++,C#,Objective-C等)他们很少支持语言扩展功能,因此着重谈一谈如何开发一个外部DSL。

编译原理基础
词法分析和语法分析
既然要开发一个外部DSL,那就免不了要涉及编译原理相关的知识了。不过放心,我们不需要自己造轮子,那是个大工程,是非常缓慢也容易出错的。本文主要介绍使用ANTLR工具来实现语言的解析。但是关于编译原理,我们还是得复习一下它的基本概念。

一个完整的编译器包括词法分析、语法分析、语义分析、源代码优化、代码生成、目标代码优化等步骤。词法分析和语法分析是编写一个外部DSL所必须涉及的部分。词法分析的输入是源程序字符串,其功能是一个一个字符地读取字符串,从中识别出标识符、关键字、常量等相对独立的记号(Token),形成记号流的过程。例如c,l,a,s,s五个字符构成了class关键字,2,3构成了整数23。词法分析器会过滤掉换行,空格,注释与程序处理无关的字符,并将记号(Token)归类。

语法分析是根据词法分析出的记号流,分析出源程序的语法结构,并按照语法结构生成语法树的过程。语法树的叶子节点就是词法分析得到的记号。例如,有下面源程序:
class T {
    string Name;
    object GetValue() {
    }
}

经过词法分析之后的结果为:
class T { string Name ; object GetValue ( ) { } }

而语法分析之后的结果为:



文法
文法是描述语言规则的工具。文法最初是一些研究自然语言的科学家总结指定的,后来被应用到计算机语言中。下面这个例子定义了人类的语法规则。
引用

语言 =>(句子)+
句子 => 主语 谓语
谓语 => 动词 宾语
主语 => 名词
宾语 => 名词
名词 =>‘张三’| ‘代码’
动词 =>‘编写’

如 ,‘张三编写代码’这句话在文法中按照LL的推导规则,其过程是:
语言 => 主语 谓语
=> 张三 动词宾语
=> 张三 编写名词
=> 张三 编写 代码

ANTLR
ANTLR是一种自动生成词法分析器、语法分析器的工具。利用它,我们不用关注词法分析和语法分析的细节。只需要描述语言的文法,将其作为输入传递给ANTLR,便可以自动生成词法分析器和语法分析器。ANTLR由Java语言写成,但是可以支持其它的目标语言,也就是,它可以生成C++、JavaScript等语言的词法分析和语法分析程序。

ANTLR有以下特点:
  • Easy:自动完成词法分析和语法分析。应用程序所做的,仅仅是遍历解析树。
  • 直观的文法描述:只要会写正则表达式,就能写文法。可维护性强。
  • 强大的功能: LL(*)语法分析器。
  • 目标代码的可读性与可扩展性:listener以及visitor遍历方式。
  • 多语言target:支持大多数主流语言。
  • 带有开发IDE: AntlrWorks。

被广泛使用: IDEA、Hibernate、Groovy、OpenJDK、Weblogic、Hive等语言都用到了它。

ANTLR的安装和使用
在ANTLR中,可以用一个jar包搞定以下事情:
  • testRig测试工具
  • 目标代码生成工具
  • 解析程序依赖的库
  • testRig测试工具输入一段符合用户定义文法的代码,输出语法树。

ANTLR会自动帮我们生成词法分析器和语法分析器,但是这两个分析器均依赖于ANTLR-Runtime代码,在Java语言里,jar包包括了Runtime代码。如果是其它目标语言,需要下载对应的Runtime。

安装ANTLR很简单,只需要从官网下载一个jar包即可。然后设置相关的命令行参数。



ANTLR工作流程
ANTLR的工作流程可以由下图表示:



语言识别器(ANTLR库)在执行的过程中,输入用户的DSL字符串,然后经过词法分析,分割成独立的token单元,然后经过语法解析器生成解析树。解析树上包含了文法规则以及词法单元。

ANTLR文法定义
文法结构

ANTLR库输入文法(.g 或者 .g4 文件),然后将文法转换为目标语言的源代码。如下图所示,文法定义的结构按照下图线条形式转换为响应的目标语言代码:



一个示例文法
grammar Expr;

program: statement;
statement: expression NEWLINE;
expression : multExpr (('+' | '-') multExpr)*  ;
multExpr : atom (('*' | '/') atom)* ;
atom:  '(' expression ')' | INT;
ID : ('a'..'z' |'A'..'Z')+ ;
INT : '0'..'9' + ;
NEWLINE:'\r'? '\n' ;
WS : (' ' |'\t' |'\n' |'\r' )+ -> skip;

上面的代码是一个示例文法。开头的第一行使用grammar关键字定义了文法的名称为Expr,这个名称也必须跟文法文件(g4文件)一致。 文法由词法规则和语法规则组成,其格式为:规则名 : 规则内容 ; 规则名与规则内容用冒号分割,每一条规则以分号分隔。规则名可以用户自定义。文法规则要求规则名的第一个字符必须为大写,上述代码中,ID,INT,NEWLINE,WS均为文法规则。语法规则要求第一个字符必须小写。

在文法规则中,可以使用类似正则表达式的描述来表达重复。
例如,在ID词法规则中,用 “ .. ” 来描述了 a到z这26个字母。ID词法规则也使用 | 以及+号运算符来表达ID是由一个或者多个大写字母或者小写字母组成。其它的规则类似正则表达式。

在语法规则中,以空格来表示各个子规则之间的“并且”关系。例如 statement: expression NEWLINE; 这条规则规定了statement必须由expression语法单元和一个NEWLINE词法单元组成,并且expression的顺序在NEWLINE之前。

常见的规则表示:

空格连接符: 表示顺序连接
| 选择符: 表示或者的关系
重复次数: + *
可选: ?
子规则: ( )
注释: // /* */
语法规则: 小写字母开头
词法规则: 大写字母开头
上述文法,如果输入“1+2*3/(4+5)”这段“DSL”,则会生成下图所示的解析树。



可以看出,叶子节点为词法单元的值,除了叶子节点,其它节点的内容都为语法名称。

避免左递归
由于ANTLR的语法分析器是LL型的,所以当它遇到左递归的文法时,会导致解析器无限递归,最终堆栈溢出,所以最好在文法层面使用 * + 等规则来表示重复。也可以将规则代入公式,算出等价的非左递归文法。



例如,有以下文法:
a : (a A) | B;
A : '3';
B : '4';

当输入为4333时,会造成解析器堆栈溢出。
解决方法:改写文法为:
 a : Bc;
 c : Ac | ;

又因为 c : Ac | ; 等价于 c: A*,所以目标文法可以改写成: a : BA*

生成解析程序
ANTLR生成解析程序很简单,只要以文法路径作为参数运行一下jar包即可。
java -jar antlr-4.5.2-complete.jar Expr.g4

这条命令会生成默认以Listener方式遍历解析树的代码。可以通过命令行参数来控制生成选项:

-DLanguageName 生成LanguageName的目标处理程序,例如-DJavaScript
-visitor 生成visitor模式遍历解析树的基类代码
-listener 生成listener模式遍历解析树的基类代码
关于Listener与Visitor的区别,下文会介绍。

使用-visitor参数运行这条命令之后,ANTLR会帮我们生成下列文件:



.tokens文件:按行分隔的词素列表及其类型。
ExprListener:遍历解析树所需要的Listener接口。
ExprBaseListener:实现Listener接口的具体类。 (Adapter模式(缺省适配器))
ExprVisitor: 使用Visitor模式遍历解析树的接口。
ExprBaseVisitor:实现Visitor接口的具体类。
ExprLexer: 词法分析器。
ExprParser:语法分析器。

解释执行DSL
Context

解释执行DSL的过程中一个重要的步骤就是遍历语法树。ANTLR会为我们在文法中写的每个语法规则生成一个对应的Context,如下图所示。



Context对象包含以下内容:
语法分析时生成

起始Token,终止Token
通过Token中的行号:列号可以在解释DSL出错的时候给用户提示
类型,text:通过类型和text可以获得Token匹配的内容。
children: 可以得到子语法规则中的内容。
异常信息: 可以得到解析失败的信息。



ANTLR为每个子规则创建一个同名函数,因此可以方便地取到其子规则的context。例如我们的statement语法规则包含expression子规则,在上图中可以看到,生成了这样的同名函数,它返回的是expression的Context。

遍历解析树

遍历解析树有两种方式,一种是Listener,另一种是Visitor。对于Listener来说,ANTLR会生成以下结构的代码:



可以看出,它为每个语法规则都生成了匹配的enter/exit 方法,并且将Context作为参数传递进去。

Listener模式是一种被动遍历模式。遍历过程在ANTLR库中,用户调用开始遍历代码之后,ANTLR将会按照文法规则递归下降地遍历语法树,在遇到语法单元的前后分别调用enter和exit方法。遍历过程由下图所示。



Listener的遍历模式虽然简单,但是不可人为控制访问过程,并且逻辑分散在enter/exit两个方法中,对于编写遍历程序有所不便,因此有另一种Visitor模式可以选择。



Visitor模式为每个语法规则生成了一个visit方法,参数为对应的Context。其缺省实现为直接visitChildren。我们可以人为地控制visitChild的时间,并且在此前后做一些事情。
@Override
public T visitProgram(ProgramContext ctx) {
    // before visit, doing something
    T result = visitChildren(ctx);
    // after visit, doing something
    return result;
}

此外,Visitor类还提供一个泛型参数T,这个参数用来返回visit的结果。我们在下文中将会演示如何使用它。



示例

理解了上文所述的知识点之后,此时我们应该可以开发出上文的Expr文法对应的解析程序了。

在下面的例子中,我们继承了ExprBaseVisitor这个ANTLR生成的类,并重写了visitExpression, visitMultExpr,visitAtom这几个方法。从visitAtom看起,我们先通过使用自动生成的与语法规则同名的expression()来判断用户输入的DSL解析之后的语法节点中是否含有expression规则,如果没有,那么直接取词法单元的文本,并将其转化为Double。如果有expression,则调用visit方法递归地进行解析。

在visitExpression()中,我们通过ctx.children来获取词法规则expression : multExpr (('+' | '-') multExpr)* ;中*号匹配的多重子规则项,然后判断Context的类型是否为TerminalNode来得知child究竟是词法单元还是语法单元,如果是词法单元,则得到操作方法是加好还是减号,如果是语法单元,则递归下降地继续求值。
private static class MyVisitor extends ExprBaseVisitor<Double>{
        @Override
        public Double visitProgram(ExprParser.ProgramContext ctx) {
            return super.visitProgram(ctx);
        }

        @Override
        public Double visitStatement(ExprParser.StatementContext ctx) {
            return super.visitStatement(ctx);
        }

        @Override
        public Double visitExpression(ExprParser.ExpressionContext ctx) {
            String lastOp = "+";
            double result = 0;

            for (ParseTree child : ctx.children){
                if (child instanceof TerminalNode){
                    lastOp = child.getText();
                    continue;
                }
                ExprParser.MultExprContext context = (ExprParser.MultExprContext)child;
                switch (lastOp) {
                    case "+":
                        result += visitMultExpr(context);
                        break;
                    case "-":
                        result -= visitMultExpr(context);
                        break;
                    default:
                        assert false : "invalid operation type";
                        break;
                }
            }
            return result;
        }

        @Override
        public Double visitMultExpr(ExprParser.MultExprContext ctx) {
            double result = 1;
            String lastOp = "*";

            for (ParseTree child : ctx.children){
                if (child instanceof TerminalNode){
                    lastOp = child.getText();
                    continue;
                }

                ExprParser.AtomContext atomContext = (ExprParser.AtomContext)child;
                switch (lastOp) {
                    case "*":
                        result *= visitAtom(atomContext);
                        break;
                    case "/":
                        result /= visitAtom(atomContext);
                        break;
                    default:
                        assert false : "invalid operation type";
                        break;
                }
            }
            return result;
        }

        @Override
        public Double visitAtom(ExprParser.AtomContext ctx) {
            if (ctx.expression() != null){
                return visitExpression(ctx.expression());
            }
            else{
                return Double.parseDouble(ctx.INT().getText());
            }
        }
    }

调用ANTLR进行解析也很简单,首先将DSL文本作为构造函数参数传递给ANTLRInputStream,然后调用生成的ExprLexer进行词法分析,再把词法分析的结果(tokens)传递给ExprParser进行语法分析,然后调用语法分析的规则名称获取对应规则的解析树,接着对解析树进行遍历。
 private static void antlrTest(){
        String sentence = "1+2*3/(4+5)\n";
        ExprLexer lexer = new ExprLexer(new ANTLRInputStream(sentence));
        CommonTokenStream tokens = new CommonTokenStream(lexer);
        ExprParser parser = new ExprParser(tokens);

        ExprParser.ProgramContext context = parser.program();
        MyVisitor visitor = new MyVisitor();
        double result = visitor.visit(context);
        System.out.println("result=" + result);
    }

更多资料
关于Clojure/LISP提供的扩充自己语言,以实现内部DSL,可以参考这篇文章:https://zhuanlan.zhihu.com/p/20754002
IBM的DeveloperWorks有一篇文章,介绍了ANTLR的一些高级用法,比如类型检查、代码生成。甚至实现了一个虚拟机来执行生成之后的代码,地址是:https://www.ibm.com/developerworks/cn/java/j-lo-antlr-fullapp/
知其所以然
关于ANTLR,如果想要了解更多,可以阅读《The Definitive ANTLR 4 Reference》,ANTLR的作者也写了另一本书描述ANTLR的原理,叫做《编程语言实现模式》。上面的两本书都比较侧重实践。如果想继续深入研究编译原理的理论知识,可以参考《编译原理》。前两本书如果读懂了,再读第三本书也不会太费力,因为此时你已经有了很多感性认识了。

转载自:ATA

作者:苏樽
  • 大小: 58.5 KB
  • 大小: 42.9 KB
  • 大小: 64.9 KB
  • 大小: 201.2 KB
  • 大小: 118.9 KB
  • 大小: 34.3 KB
  • 大小: 11.4 KB
  • 大小: 47.2 KB
  • 大小: 28.1 KB
  • 大小: 157.5 KB
  • 大小: 196.5 KB
  • 大小: 64 KB
  • 大小: 100.6 KB
来自: 云栖社区
2
1
评论 共 2 条 请登录后发表评论
2 楼 cywhoyi 2016-06-06 11:36
ronnin 写道
这么好文章, 还是有人"踩" ?

确实写得很好,之前我也写过,感觉他写得很细致
1 楼 ronnin 2016-06-04 20:10
这么好文章, 还是有人"踩" ?

发表评论

您还没有登录,请您登录后再发表评论

相关推荐

  • 一个从撸到脚的React项目(React+Node+MongoDB).zip

    网络与通信:数据传输、信号处理、网络协议、网络与通信硬件、网络安全网络与通信是一个非常广泛的领域,它涉及到计算机科学、电子工程、数学等多个学科的知识。 云计算与大数据:包括云计算平台、大数据分析、人工...

  • 撸了一个 Feign 增强包

    点击上方IT牧场,选择置顶或者星标技术干货每日送达!前言最近准备将公司的一个核心业务系统用 Java 进行重构,大半年没写 Java ,JDK 都更新到 14 了,考虑到稳定性等问...

  • Goroutine 并发调度模型深度解析之手撸一个高性能 goroutine 池

    Scheduler2.1 线程那些事儿2.1.1 用户级线程模型2.1.2 内核级线程模型2.1.3 两级线程模型2.2 G-P-M 模型概述2.3 G-P-M 模型调度2.3.1 用户态阻塞/唤醒2.3.2 系统调用阻塞3 大规模 Goroutine 的瓶颈3.1 一个 http ...

  • 美团大神5小时撸了一个王者数据库,网友炸了

    Mysql对于程序员来说就像一个黑盒,有些人根本不知道这个黑盒的运行机制,我们经常所学到的Mysql优化技巧,其实就是一种应用技巧,而这个技巧为什么能提高查询速度是不清楚的。比如: 1. 为什么在写SQL语句时遵守...

  • 《动手学ROS2》3.5手撸一个节点Python版本

    上一节的小游戏,相信你已经运行起来了,这一节课带着你的好奇心,小鱼和你一起手撸一个Python节点。 通过本节课的学习你将有以下收获: 创建工作空间和功能包 使用非OOP方法编写一个Python节点并运行 .

  • 3天撸了一个电商平台,我飘了~

    已经7月中旬了,距离招聘的黄金季——金九银十还有一个半月,不少Java开发工程师早已摩拳擦掌,准备借此良机打好职场的翻身仗,期待实现2021新一轮的跃迁,其中不乏进军大厂的勇敢尝试。当然...

  • 技术总监亲自上阵,手撸了一门编程语言,同事直呼哇塞

    但图形学确实是特定的专业领域,我们几乎接触不到,所以对我来说换成网络更合适一些,最后再加上一个数据库。这四项技术如果都能掌握的话,可以在 IT 行业横着走了,加上这几年互联网行业越来越不景气,越底层的技术...

  • 《动手学ROS2》3.6手撸一个节点C++版

    3.5 手撸一个C++节点 作家李四来到了李家村后,下一章就要开始写小说了,光有作者没有读者可不行,所以本节我们要使用C++给李四创建一个读者节点单身汉王二。 通过本节课的学习你将有以下收获: 学会创建一个C++功能...

  • 撸了个 DDD 项目,爽!

    刚开始接触 DDD,感受最深的是「贫血模型」、「领域」、「聚合」、「值对象」这些概念很有趣,但是,在不断学习的过程中渐渐感受到:DDD 的“水”是真的深!可能每个工程师、架构师都在不断解决...

  • 学习Go语言要经历的的五个阶段

    第一个阶段(菜逼): 刚刚学习了这门语言。 已经通过一些教程或者培训班了解基本的语法,可以写短的代码片段。 第二个阶段 (探索者): 可以写一个完整的程序,但不懂一些更高级的语言特征,比如“channels”。还没有...

  • 3天我把DDD业务领域建模、数据库、聚合彻底撸干净了!

    听说,很多采用了微服务架构也不能的解决问题,都去用 DDD(领域驱动设计) 的思想去指导微服务的实践了。最近我在和一些开发人员、技术大佬交流,大家有一个普遍的感受:DDD作为一套架构方法,...

  • Goroutine并发调度模型深度解析之手撸一个协程池

    Goroutine,Go语言基于并发(并行)编程给出的自家的解决方案。goroutine是什么?通常goroutine会被当做coroutine(协程)的 golang实现,从比较粗浅的层面来看,这种认知也算是合理,但实际上,goroutine并非传统...

  • 用 Python 撸一个 Web 服务器-第1章:Web 开发简介

    不过,虽然各种框架技术日新月异,但 Web 开发的核心概念与本质依旧不曾改变,本教程将通过一个 Todo List 应用带你探索 Web 开发基本原理,只有真正明白了 Web 开发的核心基础,才能更轻松的应对新框架与技术。...

  • 撸了个低代码开发平台,爽!

    核心是做好积木(组件)本身,而实体(最终界面)恰恰不重要,对于真正的开发人员/架构师而言,开发出一个ERP、BI系统并非终点,承载核心业务、复杂应用、和多变的C端才是最真正的挑战所在。而这,恰恰是工具所不能...

  • 徒手撸一个Spring Boot中的starter,解密自动化配置

    点击上方IT牧场,选择置顶或者星标技术干货每日送达!starter背景Spring Boot目前已经变成了后端开发这必备技能之一,其中一个主要原因是Spring Boot中有个非常重...

  • 如何撸一份高薪架构级的工程师简历

    点击上方“IT平头哥联盟”,选择“置顶或者星标”与你一起成长~公众号回复[加群]进500人大群一起交流~最近有一则关于招聘相关的政策,特别是女求职者了解一下:要求在招聘时:不得以性别...

  • 春节假期撸了一个NIO框架,网友:牛X

    作为当前最流行的NIO框架之一,Netty的健壮性、功能、性能、可定制性、可扩展性在同类框架中都是首屈一指的,在互联网领域、大数据分布式计算领域、游戏行业、通信行业等获得了广泛的应用,一些...

  • 如何用Golang来手撸一个Blog - Milu.blog 开发总结

    麋鹿博客: 基于Golang, Gin, Gorm, Mysql, Vuejs 的一款轻量级个人博客

  • pyzmq-15.1.0-py2.7-macosx-10.6-intel.egg

    Python库是一组预先编写的代码模块,旨在帮助开发者实现特定的编程任务,无需从零开始编写代码。这些库可以包括各种功能,如数学运算、文件操作、数据分析和网络编程等。Python社区提供了大量的第三方库,如NumPy、Pandas和Requests,极大地丰富了Python的应用领域,从数据科学到Web开发。Python库的丰富性是Python成为最受欢迎的编程语言之一的关键原因之一。这些库不仅为初学者提供了快速入门的途径,而且为经验丰富的开发者提供了强大的工具,以高效率、高质量地完成复杂任务。例如,Matplotlib和Seaborn库在数据可视化领域内非常受欢迎,它们提供了广泛的工具和技术,可以创建高度定制化的图表和图形,帮助数据科学家和分析师在数据探索和结果展示中更有效地传达信息。

Global site tag (gtag.js) - Google Analytics