java实现编译器_实现一个简单的编译器

java实现编译器_实现一个简单的编译器简单的说编译器就是语言翻译器,它一般将高级语言翻译成更低级的语言,如GCC可将C/C++语言翻译成可执行机器语言,Java编译器可以将Java源代码翻译成Java虚拟机可以执行的字节码。编译器如此神奇,那么它到底是如何工作的呢?本文将简单介绍编译器的原理,并实现一个简单的编译器,使它能编译我们自定义语法格式的源代码。(文中使用的源码都已上传至GitHub以方便查看)。自定…

大家好,又见面了,我是你们的朋友全栈君。

简单的说 编译器 就是语言翻译器,它一般将高级语言翻译成更低级的语言,如 GCC 可将 C/C++ 语言翻译成可执行机器语言,Java 编译器可以将 Java 源代码翻译成 Java 虚拟机可以执行的字节码。

编译器如此神奇,那么它到底是如何工作的呢?本文将简单介绍编译器的原理,并实现一个简单的编译器,使它能编译我们自定义语法格式的源代码。(文中使用的源码都已上传至 GitHub 以方便查看)。

自定义语法

为了简洁易懂,我们的编译器将只支持以下简单功能:

数据类型只支持整型,这样不需要数据类型符;

支持 加(+),减(-),乘(*), 除(/) 运算

支持函数调用

支持 extern(为了调用 printf 打印计算结果)

以下是我们要支持的源码实例 demo.xy:

extern printi(val)

sum(a, b) {

return a + b

}

mult(a, b) {

return a * b

}

printi(mult(4, 5) – sum(4, 5))

编译原理简介

一般编译器有以下工作步骤:

词法分析(Lexical analysis): 此阶段的任务是从左到右一个字符一个字符地读入源程序,对构成源程序的字符流进行扫描然后根据构词规则识别 单词(Token),完成这个任务的组件是 词法分析器(Lexical analyzer,简称Lexer),也叫 扫描器(Scanner);

语法分析(Syntactic analysis,也叫 Parsing): 此阶段的主要任务是由 词法分析器 生成的单词构建 抽象语法树(Abstract Syntax Tree ,AST),完成此任务的组件是 语法分析器(Parser);

目标码生成: 此阶段编译器会遍历上一步生成的抽象语法树,然后为每个节点生成 机器 / 字节码。

编译器完成编译后,由 链接器(Linker) 将生成的目标文件链接成可执行文件,这一步并不是必须的,一些依赖于虚拟机运行的语言(如 Java,Erlang)就不需要链接。

工具简介

对应编译器工作步骤我们将使用以下工具,括号里标明了所使用的版本号:

词法分析器 制作工具,它可以根据我们定义的规则生成 词法分析器 的代码;

语法分析器 的制作工具,同样它可以根据我们定义的规则生成 语法分析器 的代码;

抽象语法树 生成目标码的过程。

在 ubuntu 上可以通过以下命令安装这些工具:

sudo apt-get install flex

sudo apt-get install bison

sudo apt-get install llvm-3.8*

介绍完工具,现在我们可以开始实现我们的编译器了。

词法分析器

前面提到 词法分析器 要将源程序分解成 单词,我们的语法格式很简单,只包括:标识符,数字,数学运算符,括号和大括号等,我们将通过 Flex 来生成 词法分析器 的源码,给 Flex 使用的规则文件 lexical.l 如下:

%{

#include

#include “ast.h”

#include “syntactic.hpp”

#define SAVE_TOKEN yylval.string = new std::string(yytext, yyleng)

#define TOKEN(t) (yylval.token = t)

%}

%option noyywrap

%%

[ \t\n] ;

“extern” return TOKEN(TEXTERN);

“return” return TOKEN(TRETURN);

[a-zA-Z_][a-zA-Z0-9_]* SAVE_TOKEN; return TIDENTIFIER;

[0-9]+ SAVE_TOKEN; return TINTEGER;

“=” return TOKEN(TEQUAL);

“==” return TOKEN(TCEQ);

“!=” return TOKEN(TCNE);

“(” return TOKEN(TLPAREN);

“)” return TOKEN(TRPAREN);

“{” return TOKEN(TLBRACE);

“}” return TOKEN(TRBRACE);

“,” return TOKEN(TCOMMA);

“+” return TOKEN(TPLUS);

“-” return TOKEN(TMINUS);

“*” return TOKEN(TMUL);

“/” return TOKEN(TDIV);

. printf(“Unknown token!\n”); yyterminate();

%%

我们来解释一下,这个文件被 2 个 %% 分成 3 部分,第 1 部分用 %{ 与 %} 包括的是一些 C++ 代码,会被原样复制到 Flex 生成的源码文件中,还可以在指定一些选项,如我们使用了 %option noyywrap,也可以在这定义宏供后面使用;第 2 部分用来定义构成单词的规则,可以看到每条规都是一个 正则表达式 和 动作,很直白,就是 词法分析器 发现了匹配的 单词 后执行相应的 动作 代码,大部分只要返回 单词 给调用者就可以了;第 3 部分可以定义一些函数,也会原样复制到生成的源码中去,这里我们留空没有使用。

现在我们可以通过调用 Flex 生成 词法分析器 的源码:

flex -o lexical.cpp lexical.l

生成的 lexical.cpp 里会有一个 yylex() 函数供 语法分析器 调用;你可能发现了,有些宏和变量并没有被定义(如 TEXTERN,yylval,yytext 等),其实有些是 Flex 会自动定义的内置变量(如 yytext),有些是后面 语法分析器 生成工具里定义的变量(如 yylval),我们后面会看到。

语法分析器

语法分析器 的作用是构建 抽象语法树,通俗的说 抽象语法树 就是将源码用树状结构来表示,每个节点都代表源码中的一种结构;对于我们要实现的语法,其语法树是很简单的,如下:

3fcb42ff6796d8be6a30b4c7bd7f742d.png

现在我们使用 Bison 生成 语法分析器 代码,同样 Bison 需要一个规则文件,我们的规则文件 syntactic.y 如下,限于篇幅,省略了某些部分,可以通过链接查看完整内容:

%{

#include “ast.h”

#include

extern int yylex();

void yyerror(const char *s) { std::printf(“Error: %s\n”, s);std::exit(1); }

%}

%token TLPAREN TRPAREN TLBRACE TRBRACE TCOMMA

%%

program:

stmts { programBlock = $1; }

;

func_decl:

ident TLPAREN func_decl_args TRPAREN block { $$ = new NFunctionDeclaration(*$1, *$3, *$5); delete $3; }

;

%%

是不是发现和 Flex 的规则文件很像呢?确实是这样,它也是分 3 个部分组成,同样,第一部分的 C++ 代码会被复制到生成的源文件中,还可以看到这里通过以下这样的语法定义前面了 Flex 使用的宏:

%token TLPAREN TRPAREN TLBRACE TRBRACE TCOMMA

比较不同的是第 2 部分,不像 Flex 通过 正则表达式 通过定义规则,这里使用的是 巴科斯范式(BNF: Backus-Naur Form) 的形式定义了我们识别的语法结构。如下的语法表示函数:

func_decl:

ident TLPAREN func_decl_args TRPAREN block { $$ = new NFunctionDeclaration(*$1, *$3, *$5); delete $3; }

;

可以看到后面大括号中间的也是 动作 代码,上例的动作是在 抽象语法树 中生成一个函数的节点,其实这部分的其他规则也是生成相应类型的节点到语法树中。像 NFunctionDeclaration 这是一个我们自己定义的节点类,我们在 ast.h 中定义了我们所要用到的节点,同样的,我们摘取一段代码如下:

class NFunctionDeclaration : public NStatement {

public:

const NIdentifier& id;

VariableList arguments;

NBlock& block;

NFunctionDeclaration(const NIdentifier& id,

const VariableList& arguments, NBlock& block) :

id(id), arguments(arguments), block(block) { }

virtual llvm::Value* codeGen(CodeGenContext& context);

};

可以看到,它有 标识符(id),参数列表(arguments),函数体(block) 这些成员,在语法分析阶段会设置好这些成员的内容供后面的 目标码生成 阶段使用。还可以看到有一个 codeGen() 虚函数,你可能猜到了,后面就是通过调用它来生成相应的目标代码。

我们可以通过以下命令调用 Bison 生成 语法分析器 的源码文件,这里我们使用 -d 使头文件和源文件分开,因为前面 词法分析器 的源码使用了这里定义的一些宏,所以需要使用这个头文件,这里将会生成 syntactic.cpp 和 syntactic.hpp:

bison -d -o syntactic.cpp syntactic.y

目标码生成

这是最后一步了,这一步的主角是前面提到 LLVM,LLVM 是一个构建编译器的框架系统,我们使用他遍历 语法分析 阶段生成的 抽象语法树,然后为每个节点生成相应的 目标码。当然,无法避免的是我们需要使用 LLVM 提供的函数来编写生成目标码的源码,就是实现前面提到的虚函数 codeGen(),是不是有点拗口?不过确实是这样。我们在 gen.cpp 中编写了不同节点的生成代码,我们摘取一段看一下:

Value *NMethodCall::codeGen(CodeGenContext &context) {

Function *function = context.module->getFunction(id.name.c_str());

if (function == NULL) {

std::cerr << “no such function ” << id.name << endl;

}

std::vector args;

ExpressionList::const_iterator it;

for (it = arguments.begin(); it != arguments.end(); it++) {

args.push_back((**it).codeGen(context));

}

CallInst *call = CallInst::Create(function, makeArrayRef(args), “”, context.currentBlock());

std::cout << “Creating method call: ” << id.name << endl;

return call;

}

看起来有点复杂,简单来说就是通过 LLVM 提供的接口来生成 目标码,需要了解更多的话可以去 LLVM 的官网学习一下。

至此,我们所有的工作基本都做完了。简单回顾一下:我们先通过 Flex 生成 词法分析器 源码文件 lexical.cpp,然后通过 Bison 生成 语法分析器 源码文件 syntactic.cpp 和头文件 syntactic.hpp,我们自己编写了 抽象语法树 节点定义文件 ast.h 和 目标码 生成文件 ast.cpp,还有一个 gen.h 包含一点 LLVM 环境相关的代码,为了输出我们程序的结果,还在 printi.cpp 里简单的通过调用 C 语言库函数实现了输出一个整数。

对了,我们还需要一个 main 函数作为编译器的入口函数,它在 main.cpp 里:

int main(int argc, char **argv) {

yyparse();

InitializeNativeTarget();

InitializeNativeTargetAsmPrinter();

InitializeNativeTargetAsmParser();

CodeGenContext context;

context.generateCode(*programBlock);

context.runCode();

return 0;

}

我们可以看到其调用了 yyparse() 做 语法分析,(yyparse() 内部会先调用 yylex() 做 词法分析);然后是一系列的 LLVM 初始化代码,context.generateCode(*programBlock) 是开始生成 目标码;最后是 context.runCode() 来运行代码,这里使用了 LLVM 的 JIT(Just In Time) 来直接运行代码,没有链接的过程。

现在我们可以用这些文件生成我们的编译器了,需要说明一下,因为 词法分析器 的源码使用了一些 语法分析器 头文件中的宏,所以正确的生成顺序是这样的:

bison -d -o syntactic.cpp syntactic.y

flex -o lexical.cpp lexical.l syntactic.hpp

g++ -c `llvm-config –cppflags` -std=c++11 syntactic.cpp gen.cpp lexical.cpp printi.cpp main.cpp

g++ -o xy-complier syntactic.o gen.o main.o lexical.o printi.o `llvm-config –libs` `llvm-config –ldflags` -lpthread -ldl -lz -lncurses -rdynamic

如果你下载了 GitHub 的源码,那么直接:

cd src

make

就可以完成以上过程了,正常会生成一个二进制文件 xy-complier,它就是我们的编译器了。

编译测试

我们使用之前提到实例 demo.xy 来测试,将其内容传给 xy-complier 的标准输入就可以看到运行结果了:

cat demo.xy | ./xy-complier

也可以直接通过

make test

来测试,输出如下:

define internal i64 @mult(i64 %a1, i64 %b2) {

entry:

%a = alloca i64

%0 = load i64, i64* %a

store i64 %a1, i64* %a

%b = alloca i64

%1 = load i64, i64* %b

store i64 %b2, i64* %b

%2 = load i64, i64* %b

%3 = load i64, i64* %a

%4 = mul i64 %3, %2

ret i64 %4

}

Running code:

11

Exiting…

可以看到最后正确输出了期望的结果,至此我们简单的编译器就完成了。

参考

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请联系我们举报,一经查实,本站将立刻删除。

发布者:全栈程序员-站长,转载请注明出处:https://javaforall.net/156461.html原文链接:https://javaforall.net

(0)
上一篇 2022年7月7日 上午7:46
下一篇 2022年7月7日 上午7:46


相关推荐

  • springboot模型中Long转String 注解实现

    springboot模型中Long转String 注解实现springboot模型中Long转String注解实现@JsonSerialize(using=ToStringSerializer.class)@JsonSerialize(using=ToStringSerializer.class)

    2022年5月15日
    79
  • PID控制原理详解(一)[通俗易懂]

    PID控制原理详解(一)[通俗易懂]PID的理解       关于理解PID控制算法最典型的一个例子就是一个漏水的水缸的问题。网上有很多讲解PID的帖子会讲到这个例子。这里我也把我自己对于PID的理解用这个例子阐述一遍。       有个漏水的水缸,而且漏水的速度还不是恒定的。然后我们还有个水桶,我们可以控制往水缸里面加水或者从水缸里面舀水出来。另外我们可以检测水平面。现在我们的目的就是要控制水平面稳定在我们想要的任何一个平面上…

    2022年5月20日
    43
  • 无序链表排序_双向链表排序算法

    无序链表排序_双向链表排序算法需求给定一个无序的链表,输出有序的链表。分析链表排序比较特殊,由于不连续的性质,所以在进行排序的时候要引入外排思想,因此归并排序或者多路归并就成为了排序的选择。归并排序分为拆分、合并两个阶段:1.拆分需要拆分出链表中间节点,并赋值NULL阶段,形成两个独立的链表,直到拆分成单个节点为止。2.合并由于此时没个链表都为单节点,所以实质上是个有序链表合并问题。代码下面

    2022年10月11日
    4
  • SecureCRT的使用方法和技巧(详细使用教程)[通俗易懂]

    SecureCRT的使用方法和技巧(详细使用教程)[通俗易懂]随时随地阅读更多技术实战干货,获取项目源码、学习资料,请关注源代码社区公众号(ydmsq666)SecureCRT是最常用的终端仿真程序,简单的说就是Windows下登录UNIX或Liunx服务器主机的软件,本文主要介绍SecureCRT的使用方法和技巧VanDykeCRT和VanDyke…

    2022年6月7日
    61
  • div内图片和文字水平垂直居中「建议收藏」

    div内图片和文字水平垂直居中「建议收藏」大小不固定的图片、多行文字的水平垂直居中本文综述想必写css的都知道如何让单行文字在高度固定的容器内垂直居中,但是您知道或者想过让行数不固定的文字在高度固定的容器内垂直居中呢?本文将会告诉你如何实现多行文字的垂直居中显示。关于图片垂直居中显示,想必很多写css的人都研究过,或者说是搜寻过方法。淘宝团队似乎提供了一种不错的方法,用font-size解决IE下垂直居中的问题,是

    2025年6月26日
    4
  • DDoS 攻击防御方法

    DDoS 攻击防御方法网络层 DDoS 防御限制单 IP 请求频率 网络架构上做好优化 采用负载均衡分流 防火墙等安全设备上设置禁止 ICMP 包等 通过 DDoS 硬件防火墙的数据包规则过滤 数据流指纹检测过滤 及数据包内容定制过滤等技术对异常流量进行清洗过滤 采用 ISP 近源清洗 使用电信运营商提供的近源清洗和流量压制 避免全站服务对所有用户彻底无法访问 这是对超过自身带宽储备和自身 DDoS 防御能力之外超大流量的补充性缓解措施 应用层 DDoS 防御优化操作系统的 TCP IP 栈 应用服务器严格限制单

    2026年3月18日
    2

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注

关注全栈程序员社区公众号