打破国外垄断,开发中国⼈⾃⼰的编程语⾔(1):实现可以解
析表达式的计算器
用java编写一个简单的计算器
阅读本系列⽂章将是“最残酷的头脑风暴,⼤家做好准备了吗”
本⽂是《打破国外垄断,开发中国⼈⾃⼰的编程语⾔》系列⽂章的第1篇。本系列⽂章的主要⽬的是教⼤家学会如何从零开始设计⼀种编程语⾔(marvel语⾔),并使⽤marvel语⾔开发⼀些真实的项⽬,如移动App、Web应⽤等。marvel语⾔可以通过下⾯3种⽅式运⾏:
1. 解释执⾏
2. 编译成Java Bytecode,利⽤JVM执⾏
3. 编译成⼆进制⽂件,本地执⾏(基于LLVM)
本系列⽂章实现的marvel语⾔并不像很多《⾃⼰动⼿》系列⼀样,做⼀个玩具。marvel语⾔是⼀个⼯业级的编程语⾔,与kotlin、Java等语⾔是同⼀个级别,设计之初是为了试验编程语⾔的新特性。我们团队开发的超平台开发系统UnityMarvel内嵌的Ori语⾔的部分特性也是来源于Marvel。关于UnityMarvel的细节后⾯会专门写⽂章介绍。这⾥先讨论编译器的问题。
1. 如果系统软件受到制约,有没有可能突出重围呢?
我们知道,现在中美贸易战如⽕如荼,可能以后使⽤国外很多软件,尤其是系统软件,都会有⼀些问题。这就需要我们在⼀些关键领域有⾃⼰可以控制的技术和软件,例如,操作系统、编程语⾔、数据库、科学计算软件等。其实这些种类的软件中,⼤多都属于基础软件,只有操作系统和编程语⾔(以及相关的IDE)可以称为是系统软件。
这⾥先说说基础软件和系统软件的区别。基础软件是指很多软件都依赖的软件,例如,流⾏的程序库(如tensorflow、pytorch等)、数据库(如MySQL、Oracle等)。但⼤多数基础软件的⼀个共同特点是只服务于特定领域,例如,你不可能⽤MySQL开发⼀款游戏,也不可能⽤tensorflow开发移动App。⽽在基础软件中有⼀⼩类,它们是通⽤的,⼏乎适合于各个领域,我们将这类软件称为系统软件。它们是整个IT领域的基础架构。没有它们,整个IT领域将不复存在。例如,⽬前,只有操作系统和编译器符合这两个特征。⼤家可以想想,没有了关系型数据库,还有其他类型的数据库可以使⽤,没有了tensorflow,IT领域也不会停⽌运转。但没有了Windows、macOS、Linux、C语⾔、Java语⾔这些技术,世界将会怎样,将会重新退回到⼯业⽂明时代。所以系统软件是基础软件的⼀个⼦集,⽽且必不可少。如果将基础软件和其他软件⽐作星球,那么系统软件就是星核。
在系统软件中,编译器是最容易突破的。因为编译器(编程语⾔)的⽣态相⽐操作系统来说,更容易建
⽴。这是因为⽬前有很多虚拟机可以选择,例如,最常⽤的是JVM,当然,还有微软的 core等技术。如果我们的编程语⾔可以基于JVM,那么就意味着可以利⽤Java语⾔的所有⽣态,如果我们的编程语⾔可以⽤更容易的⽅式调⽤其他语⾔(如C++、Go等),在某种程度上,也就可以直接使⽤这些编程语⾔的⽣态。当然,还有更先进的超⽣态技术(UnityMarvel的Ori语⾔正是基于超⽣态技术的),总之,作为⼀种新的编程语⾔,利⽤其他的⽣态是最廉价的⽅式,当然,在语⾔发展的过程中,也可以逐渐建⽴⾃⼰的⽣态(相当于骑驴马),这也是⼀种策略。所以如果想突破,编译器(编程语⾔)是最容易的⼀个。当然,如果拥有⾃⼰可以控制的编程语⾔,可以为后期的操作系统提供⽀援,例如,利⽤超⽣态技术,在建⽴新操作系统之前,就为该操作系统提前建⽴⽣态(这⼀点以后专门撰⽂阐述)。
2. 开发编程语⾔需要哪些知识
现在进⼊到最关键的部分了,开发⼀种编程语⾔到底需要哪些知识呢?其实需要的知识还是蛮多的。最基础的要求是必须⾄少会⼀种编程语⾔。如C、C++、Java、C#、Go、Python等。当然,推荐会3种以上的编程语⾔,因为我们是在设计编程语⾔,不是在设计普通的软件。在设计编程语⾔时,需要进⾏横向⽐较,也就是需要参考其他的编程语⾔,因为任何新技术都不可能100%完全凭空产⽣,这些新技术都会或多或少地留下其他同类技术的影⼦,编程语⾔也不例外。例如,UnityMarvel内嵌的Ori语⾔就是参考了数⼗种编程语⾔,以及加⼊了⾃⼰的新技术⽽最终形成的。
除了要了解⼤量的编程语⾔外,还有很多与业务有关的知识需要掌握。主要的知识结构(不仅仅这些,后⾯⽤到了再详细讲)如下:
(1)了解⼤量的编程语⾔(推荐3种以上)
(2)编译原理的基础知识
(3)算法能⼒
(4)编译器前端⽣成器
(5)学习能⼒
(6)想象⼒
尽管开发编程语⾔并不会像⼤学学的编译原理⼀样从0开始构造⼀个编译器,但编译原理的基础知识还是要掌握的,不了解编译原理的同学,赶紧上B站、西⽠视频、油管去补课,后期我也会结合marvel语⾔做相关的视频课程,⼤家可以关注哦!
算法就不必说了,编译器⾥⾯充斥着各种算法,编译器的算法密度⼏乎超过了绝⼤多数应⽤。任何形式
的算法都可能涉及到,最基础的数据结构必须掌握,其他的算法,能学多少就学多少,多多益善。这个没有固定的教程,也是需要不断在实践中学习。
开发编译器的基本步骤如下图所⽰。
⾸先说明⼀点,并不是所有的编译器都严格按照这些步骤进⾏,有可能会将多个步骤合成⼀个步骤(例如,语法分析和语义分析合成⼀步,最后输出AST),也有可能将⼀步分成多个步骤,或者再增加⼀些与业务相关的步骤。
对于⼯业级编译器来说,并不会从0开始实现词法和语法分析器,并不是这东西有多难,⽽是如果完全⼿⼯编写代码,要添加或修改⼀个新语法,那简直就是⼀场噩梦,因为要修改⾮常多的地⽅,⽽且⼀旦出错,⾮常不好原因(因为代码过于复杂)。由于词法分析和语法分析有规律可循,所以出现了很多通过⽂法⽣成词法分析器和语法分析器的⼯具,由于词法分析与语法分析是编译器前端的重要组成部分,所以这类⼯具通常称为“编译器前端⽣成器”。⽐较著名的包括lex、yacc、javacc、antlr等。其中lex是专门⽤来⽣成词法分析器的,yacc⽤来⽣成语法分析器的,javacc可以同时⽣成词法和语法分析器、antlr也同样可以⽣成词法分析器和语法分析器。不过lex和yacc只⽀持C语
⾔,javacc只⽀持Java语⾔。⽽antlr⽀持多种编程语⾔,例如Java、C++、JavaScript、Go、C#、Swift等。本系列⽂章也使⽤了antlr的最新版本antlr4来实现编译器的前端(词法分析器和语法分析器)。
这⼏种⼯具都是依赖于⽂法⽣成词法分析器和语法分析器的,例如,在antlr4中,如果要识别加减乘除四则运算,只需要编写下⾯的⽂法即可。
expr: expr op=('*'|'/') expr | expr op=('+'|'-') expr
⽂法是不是很简单呢?但如果要编写完善的代码,可能需要上百⾏才能实现(我们团队实现的Ori语⾔,利⽤antlr4⽣成的词法和语法分析器,总共6万⾏Go语⾔代码,我们⾃⼰编写了⼤概4万⾏Go代码,整个编译器有超过10万⾏代码,3/5是⾃动⽣成的,2/5是⾃⼰编写的)。⽽且⽂法还标识了优先级,antlr4规定,写在前⾯的⽂法的优先级⾼于写在后⾯的⽂法的优先级。我们知道,对于四则运算来说,是先乘除,后加减,所以expr op=('*'|'/') expr 应该在expr op=('+'|'-') expr 前⾯,倒过来是不⾏了。如果要加更复杂的运算,例如,平⽅、开⽅、幂等,只需要修改这个⽂法即可,是不是很简单呢?
前⾯说的前4点是硬知识,也有很多教程可以学习,但最后两点:学习能⼒和想象⼒,就要完全靠⾃⼰的天赋了。因为前⾯4点能让你做出⼀个看着还不错的编译器,但最后两点能决定你做的编译器有多强⼤。
实现⼀个编程语⾔,所涉及到的知识要⽐实现编译器难度更⼤。因为如果实现编译器,并且是已经存在的编程语⾔,由于语法已经确定,所以只需要实现出来即可。但编程语⾔不同,⼀切需要重新设计,尤其是在涉及到新语法时,⾮常困难,需要了解的知识相当多,所以需要拥有快速学习能⼒,可以在短时
间内学会并掌握任何知识和技术。另外,想象⼒更重要,因为设计⼀款新的编程语⾔,有些东西可能不仅仅局限于IT领域,也不仅仅局限于⾃⼰所从事的技术领域,例如。在Ori语⾔中,拥有⼀些创新的语法,需要同时适应类似JavaScript的单线程模式和Java的多线程模式。因此,拥有多维度的想象⼒才是最终取得胜利的关键。
3. ⾃⼰设计的编程语⾔会流⾏吗
我经常在⽹上看到很多同学在问,为什么中国没有⾃⼰流⾏的编程语⾔(尽管有易语⾔,但由于是中⽂编程,所以注定不会全球流⾏,国内也并不算流⾏)呢?BAT等⼤⼚为何不开发⼀个呢?然后有⼈回答,开发编程语⾔容易,关键是⽣态,还有⼈回答,BAT是因为没有必要,因为编程语⾔没有和KPI挂钩,也有些⼈回答,开发⼀款编程语⾔,⽕起来很难。其实这些都可能是原因,但主要原因其实就是需求没有与⾏动挂钩,或者说,现在的编程语⾔已经⾜够满⾜需求了,没有必要再开发⼀款新的编程语⾔,⽽且这些⼤⼚的盈利压⼒都很⼤,当然,还有技术积累的问题。
其实编程语⾔有很多种,有⼀种就是像Java、C#、C++⼀样的通⽤编程语⾔,这类语⾔什么都能做,是⼀种图灵完备的编程语⾔。还有另外⼀种编程语⾔,如SQL、VBA、ABAP(SAP的内嵌语⾔),这类属于领域编程语⾔,他们也可能是图灵完备的,也可能不是图灵完备的。通常使⽤这类编程语⾔完成某些特定的⼯作,如SQL操作数据库,VBA操作Office、ABAP操作SAP数据等。其实在国内有很多公司内部已经提供了类似的领域语⾔,只是⾮常专业,功能单⼀,绝⼤多数⼈不清楚⽽已。
⾄于⾃⼰开发出来的编程语⾔是否会流⾏,其实你们想太多了。编程语⾔是为了解决实际问题⽽存在的,不是为了流⾏⽽存在的。就像⾐服,最初的⽤途是为了保暖,⽽不是时尚,当⼤多数⼈都使⽤⾃⼰⽣产的⾐服保暖,那他就是流⾏款了!所以让编程语⾔解决实际问题才是优先要考虑,⾄于以后是否会流⾏,⾃⼰说了不算!
像我们团队开发的UM系统,其实原来压根就没打算⾃⼰开发编程语⾔,想直接使⽤JavaScript,不过后来发现,JavaScript太动态了,使⽤JavaScript根本没有办法做⼀款完美的IDE,⽽且功能有限,并且混乱。还有就是JS是动态语⾔,如果将其转换为静态语⾔,会以牺牲性能
为代价,⽽且⽆法有效融合单线程和多线程的特性,并且还⽆法与UM IDE融为⼀体,所以没办法,才开发⼀款⾃⼰的编程语⾔Ori,并且融合了数⼗种编程语⾔的优秀特性,⽽且加⼊了更先进的特性(如内嵌SQL、虚拟组件、虚拟数据库、⽀持跨平台的语法、客户端服务端⼀体化、柔性热更新等),当然,这些特性需要与UM IDE配合才能使⽤。
4. 开发编程语⾔,从这⾥起航:配置Antlr4环境
如果⼀上来就开发编程语⾔,估计⼤家就开始晕了,所以我们先从最简单的开始,就是先来编写⼀个可以解析加减乘除表达式的编译器。我们使⽤了antlr4来⽣成词法分析器和语法分析器,所以先要配置⼀下antlr4的开发环境。
由于antlr4使⽤Java开发,所以不管⽤什么编程语⾔设计编译器,JDK必须安装,并且还需要⼀款强⼤的Java IDE,这⾥推荐Intellij IDEA。我们只使⽤Intellij IDEA的最基础功能,所以CE(社区版)版⾜够了,这个版本是免费的。
在安装完Intellij IDEA CE后,到下⾯的页⾯下载antlr4⼯具相关的库。
进⼊页⾯,到下⾯的部分,点击第1个链接下载即可。
下载完antlr4的⼯具包后,到其中的Java运⾏时库,并⽤Intellij IDEA CE创建⼀个Java⼯程,然后直接将Antlr4 Java运⾏时库复制到⼯程的lib⽬录中(没有lib⽬录可以建⽴⼀个),如下图所⽰。
然后在lib⽬录的右键菜单中点击“Mark Directory as”>“Sources Root”菜单项,将lib编程源代码⽬录,这样Intellij IDEA CE就会搜索lib⽬录中的所有库。当然,可以直接在模块中引⽤antlr4的库,不过将antlr4 运⾏时库与⼯程放到⼀起,这样如果将⼯程复制到其他机器上,就不会由于antlr4的运⾏库没有复制⽽导致⽆法运⾏了。
然后需要安装Intellij IDEA CE的Antlr插件。进⼊插件安装页⾯,如果没有安装antlr插件,选择Marketpla
ce标签页,输⼊antlr搜索插件,通常第⼀个就是。点击右侧的install按钮即可安装。如果已经安装,Antlr插件会出现在Installed页⾯中,如下图所⽰。
安装完Antlr插件后,新创建⼀个⽂件,将⽂件扩展名设置为g4,就会看到⽂件前⾯的图标变成了红⾊,⾥⾯有⼀个A字母,这就是Antlr4的标识,如下图所⽰。
5. Antlr4的Hello World
现在我们开始进⼊激动⼈⼼的时刻了,⽤Antlr4亲⼿做我们的第⼀个编译器:解析四则运算表达式的计算器。不过在完成这个编译器之前,⼀定要了解⼀下Antlr4。
下⾯先给出⼀个可以识别以hello开头的词组的识别程序的⽂法。⾸先创建⼀个名为Hello.g4的⽂件,并输⼊下⾯的代码:
grammar Hello;
r  : 'hello' ID ;
ID : [a-z]+ ;
WS : [ \t\r\n]+ -> skip ;
⼤家先不需要管这些代码是什么意思,只需要照猫画虎输⼊即可。
然后在Hello.g4右键菜单点击“Configure ANTLR”菜单项,会弹出如下图的对话框,设置第⼀个⽂本输⼊框,指定⽣成⽬录,这⾥指定与Hello.g4相同的⽬录。Hello.g4⽣成的⽂件都会放在这个⽬录中。
然后点击Hello.g4右键菜单的“Generate ANTLR Recognizer”菜单项,会⾃动⽣成⼀堆⽂件,如下图所⽰。注意:Java⽂件都隐藏了扩展名。
Hello.java和MyHelloVisitor.java是后来创建的,其他⽂件都是⾃动⽣成的。其中HelloLexer.java是词法分析器、HelloParser.java是语法分析器,其他⽂件后⾯再说。
⼤家可以打开这两个⽂件,看到每⼀个⽂件的内容都有上百⾏,这要是⼈⼯编写,会累死⼈,⽽使⽤Antlr4,只需要4⾏⽂法就搞定。如果要添加或修改原来的语法,只需要修改Hello.g4⽂件,然后再重新⽣成⼀遍即可。
现在有⼀个问题,怎么⽤Hello.g4⽣成的⼀堆⽂件呢?或者换种问法,⽣成的这些⽂件有什么⽤呢?
Hello.g4⽣成的这些⽂件的主要⽬的就是进⾏词法分析和语法分析,那么如何⽤呢?使⽤有如下两种⽅式:
1. ⽤grun⼯具测试
2. ⽤Java代码调⽤词法分析器和语法分析器,编写完整的编译器
现在先来说说grun⼯具。其实并没有grun这个东西,grun是⼀个别名,真实的⼯具在是antlr-4.8-complete.jar中的 org.antlr.v4.gui.TestRig 类,在macOS或Linux下,可以使⽤alias命令起⼀个别名,官⽅叫grun,所以这⾥就沿⽤了官⽅的叫法。如果在windows下,可以创建⼀个d⽂件。
起别名的完整命令如下:
alias grun='java -classpath .:/System/Volumes/Data/sdk/compilers/antlr4-4.8/antlr-4.8-complete.jar org.antlr.v4.gui.TestRig'
现在就可以使⽤grun测试我们的程序了。
⾸先要说明⼀点,grun测试的是.class⽂件,不是.java⽂件,所以在测试之前,要在终端中切换到.class⽂件所在的⽬录。Intellij IDEA CE默认的.class⽬录是out/production⽬录,如下图所⽰。在⼀开始,前⾯⽣成的.java⽂件并没有编译,读者可以随便个Java程序运⾏下,这时Intellij IDEA CE会编译所有还没有编译的.java⽂件,我们会发现,刚才⽣成的所有.java⽂件都⽣成了同名的.class⽂件。