原文
介绍
除此外,LLVM
是编译器编写者的平台.因为其异常干净和小巧的IR
(中间表示),使用LLVM
编写编译器比其他系统容易得多.
作为证明,栈机
的作者在大约四天内编写了整个编译器(语言定义,词法解析器,解析器,代码生成器
等)!了解这一点很重要,因为它显示了使用LLVM
时,可多快
地获得一门新语言
.
此外,这是作者使用LLVM
创建的第一门语言.学习曲线包含在这四天中.
这里描述的栈机
语言,类似Forth
.程序
是定义单词
的简单集合
,定义
只能操作栈
或生成I/O
.这很简单.虽然计算
上是完整的,但不实用.
然而,它是完整
的,它很简单
,而且它没有类似C的语法,这使得它对演示目的很有用.它表明LLVM
可应用至多种语言.
栈机
背后的基本概念
非常简单.程序操作整数
(或符指针
)栈.程序只能操作栈
并执行有限的I/O
操作.语言为你提供了几个内置
单词,按有趣的方式操作栈.
以下是在栈机
中编写传统"Hello,World"
程序的方法:
: hello_world "Hello, World!" >s DROP CR ;
: MAIN hello_world ;
这有两个"定义"(栈机
操作有定义的单词
,而不是函数
):MAIN
和hello_world
.
MAIN
定义是标准的;它告诉栈机
从哪开始.在此,按简单地调用hello_world
单词定义开始MAIN
.
hello_world
定义告诉栈机,把"Hello,World!"
串推送到栈上,并打印出来(>s)
,并从栈中弹出(DROP)
,最后打印回车符(CR)
.
尽管hello_world
使用栈,但其净效果
为空.写得好的栈机
定义,应该这样.
读者练习:你如何把它变成一个单行程序
我从LLVM
中学到的经验教训
注意到大多数重要的LLVMIR
(中间表示)的C++
类都从Value
类继承.仅当我开始为栈机
构建可执行式
时,才完全理解该简单设计
.
这确实使你的编程速度
更快.考虑为以下C/C++
式编译代码:(a|b)*((x+1)/(y+1))
.假设在栈上,这些值按a,b,x,y
的顺序排列,可在栈机中表示为:1+SWAP1+/ROT2 OR *
.你可如下用LLVM
编写个函数
来计算此式:
Value* expression(BasicBlock*bb, Value* a, Value* b, Value* x, Value* y )
{Instruction* tail = bb->getTerminator();ConstantSInt* one = ConstantSInt::get( Type::IntTy, 1);BinaryOperator* or1 =BinaryOperator::create( Instruction::Or, a, b, "", tail );BinaryOperator* add1 =BinaryOperator::create( Instruction::Add, x, one, "", tail );BinaryOperator* add2 =BinaryOperator::create( Instruction::Add, y, one, "", tail );BinaryOperator* div1 =BinaryOperator::create( Instruction::Div, add1, add2, "", tail);BinaryOperator* mult1 =BinaryOperator::create( Instruction::Mul, or1, div1, "", tail );return mult1;
}
"好吧,但没什么大不了,"你说.但,这是件大事
.原因如下.注意,我不必告诉此函数
,正在传入的是哪种类型
的值.它们可以是指令,常量,全局变量
等.
此外,如果为此操作序列
指定了错误的值,LLVM
编译会立即(编译时)注意到,或LLVM
验证器在运行时发现不一致.
这样,不会传递错误类型到生成
程序中.确实可帮助编写总是生成正确代码
的编译器
!
第二点是,不必担心分支,寄存器,栈变量,保存部分结果
等.创建指令
就是使用的值
.注意,上面代码中创建的所有内容
都是一个常量
值和五个符号
.
每个指令
都是该指令
的结果值
.这样可节省大量时间
.
教训是:SSA
形式非常强大:值
和创建它的指令
间没有区别.完全由LLVMIR
执行.利用它来发挥你最大的优势.
终止这些块!
规则:必须用终止指令
(分支,返回
等)终止编译器
中的所有BasicBlock(基本块)
.
终止指令
是LLVMIR
的语义
要求.不能按函数出现的顺序
,把函数中的块
隐式链接
在一起.一般,因为递归编写编译器
,不会按执行顺序把块
添加到函数
中.
此外,如果不终止
块,可编译.但只能在LLVM
验证器上失败时才会发现问题
.
具体块
尽早创建
你的块.编写编译器时,会遇见以下几种情况,这时,明显需要有多个块.如,C/C++
中的if-then-else,switch,while
和for
语句都需要多个块
来在llvm
中表达.规则是,尽早创建
它们.
尽早终止
块.避免忘记终止
块
用getTerminator()
来插入指令
.许多Instruction
类构造器
都带可选的insert_before
参数.
显然,正常模式的插入指令
是,在其他指令
之后插入一个指令,而不是在之前
.
但是,如果坚持终止指令
(或在BasicBlock
上使用方便的getTerminator()
方法),则总是可给指令构造器的insert_before
参数使用.
导致指令自动
插入到终止指令
前的RightPlace
位置.好处是,无需知道之前
有哪些指令
,就可传递
块并把新指令插入
其中.
这样的编译器设计
非常干净.流程:
BasicBlock* bb = new BasicBlock();
bb->getInstList().push_back( new Branch( ... ) );
new Instruction(..., bb->getTerminator() );
为此,考虑典型的if-then-else
语句(见栈机Compiler::handle_if()
方法).可如下使用LLVM
在单个函数中设置
它:
using namespace llvm;
BasicBlock*
MyCompiler::handle_if( BasicBlock* bb, SetCondInst* condition )
{//创建块以包含`if/then/else`结构中的代码BasicBlock* then = new BasicBlock();BasicBlock* else = new BasicBlock();BasicBlock* exit = new BasicBlock();//插入`"if"`的分支指令bb->getInstList().push_back( new BranchInst( then, else, condition ) );//设置终止指令then->getInstList().push_back( new BranchInst( exit ) );else->getInstList().push_back( new BranchInst( exit ) );//填充`则部分`.为了简洁,删去了细节this->fill_in( then );//填充其他部分`..为了简洁,删去了细节this->fill_in( else );//向调用者返回一个,可用`if/then/else`构造后面的`代码`填充的块.return exit;
}
上面,调用"fill_in"
方法,会添加"then"
和"else"
部分的指令.他们几乎只使用第三部分
(在终止块
前插入新指令
).
此外,如果遇见另一个if/then/else
语句,甚至可递归回handle_if
,且会工作.
注意,这一切是多么的干净利落
.特别是BasicBlock
指令列表中的push_back
方法.这些是Instruction
类型的列表,它们也恰好是值
.
为了创建"if"
分支,只需实例化要分支的块
和分支条件
为参数的BranchInst
.块
与分支标签
行为一样!
此新BranchInst
终止按参数
提供的BasicBlock
.为让调用者在调用handle_if
后继续
插入,创建了一个返回
给调用者的"退出"块
.
注意,"exit"
块用作"then"
和"else"
块的终止块
.保证了无论"handle_if"
或"fill_in"
干什么,最终都会到达"退出(exit)"块
.
狡猾的GetElementPtrInst
1,GetElementPtrInst
为最后索引内容
返回一个值
.
2,LLVM
中的所有全局变量
都是指针
.
3,还必须用GetElementPtrInst
指令解引用
指针.
即,当在(假设它是个结构或数组
)全局变量
中查找
元素时,必须首先尊重
指针!因此:
std::vector index_vector;
index_vector.push_back( ConstantSInt::get( Type::LongTy, 0 );
// ...压其他索引...
GetElementPtrInst* gep = new GetElementPtrInst( ptr, index_vector );
如,假设有个类型为[24 x int]
的全局变量
.变量自身表示该数组
指针.为了引用
数组,需要两个索引
,而不仅是一个.
第一个(0)
索引解引用
指针.第二个索引
为数组下标
.
常量很容易!
1,常量是可以是指令
符号的值
.
2,可用ConstantInt,ConstantSInt
和ConstantUInt
类的静态"get"
方法,创建经常需要的整数常量
.好处是,可快速取得
任意类型的整数
.
3,Constant
类上有个允许取类型
的null
常量的特殊方法
.初化大型数组或结构
等时非常方便
.