原文
介绍
除此外,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常量的特殊方法.初化大型数组或结构等时非常方便.