安装QQ的热更新方案,涉及到编译了,自己也试了一下,修复脚本->runtime执行,必然中间需要脚本的解释过程。
参考
1. 自定义编程语言的实现
详细解释了如何创建1门语言,但是有些错误,有些讲的不清楚,文章对应了一个demo。
demo代码研究
主要是2布:生成ast,解释执行ast。为什么需要生成ast,不直接解释执行源代码代码?做不到,比如1+2*3,直接解释执行,那么会先执行1+2,也许你会说,通过各种判断,可以先执行2+3,但是生成ast,本身就解决了执行顺序的问题,所以应该先生成ast,再解释执行,而且解耦。
生成器和解释器的代码几乎都是递归调用
第一步:生成ast
x = document.getElementById("bnf");
var rules = bnf.parse_rules(x.value); // 将string类型的bnf,parse为rules对象
console.log(JSON.stringify(rules));
ast = bnf.parse_string(y.value.trim(), rules); // y为程序代码,将string类型的程序,通过rules,parse为ast
解析 bnf 规则
bnf:
<程序> ::= <定义 块> <语句 块>
<定义 块> ::= <定义> | <定义> <定义 块>
<定义> ::= 定义 <变量 组> ;
bnf有个特点:自顶向下的设计,上面的定义依赖下面,除了语句块,字符串,应该是为了遍历的方便。
解析结果rules:
{
"<程序>": [
["<定义 块>", " ", "<语句 块>"]
],
"<定义 块>": [
["<定义>"],
["<定义>", " ", "<定义 块>"]
],
"<定义>": [
["定义", " ", "<变量 组>", " ", ";"]
],
利用rules解析程序,生成ast
基本原理: 一次又一次的遍历bnf,遍历到非尖括号,就去匹配代码。
例:
代码:定义 B,Y;
生成过程:遍历bnf,遍历到非尖括号的定义,匹配代码,匹配成功,写入ast,pos+2,重新遍历bnf,遍历到定义,匹配代码B失败,继续遍历,遍历到<字符>的定义A|B中的B,匹配成功,pos+1,重新遍历bnf,诸此类推。字符>
第二步:解释器interpreter
interpreter_ch.js 封装了2个函数: CORE.printer、CORE.interpreter
try {
p.value = bnf.operate(ast, CORE.printer); // 反向过程,将程序代码打印出来
} catch (e) {
p.value = "An unexpected error occurred during pretty-printing.";
}
console.log("p.value:\n"+p.value);
try {
q.value = "";
console.log("r.value:\n"+r.value);
CORE.interpreter.input = r.value;
var res = "\nProgram returned " + bnf.operate(ast, CORE.interpreter);
console.log("res:\n"+res);
// q.value += res;
} catch (e) {
q.value = "An error occurred while running your code.\n\n" + e + "\n\nStack trace (if supported):\n" + e.stack;
}
代码的打印:CORE.printer
封装了bnf各规则的打印函数,从ast的树根开始,递归遍历树,打印源代码
代码的执行:CORE.interpreter
封装了bnf各规则的执行函数,从ast的树根开始,递归遍历树,执行源代码,symbolTable作为变量的存储表。
例:定义函数,最终执行的是symbolTable['B'] = null; symbolTable['Y'] = null;
,结果是:symbolTable:{B: null, Y: null}
"<定义>": // Declares variables, adding them to our symbol table.
function (ast, ops) {
var ids = operateRule(ast.ast[2], ops, true);
if (!ids || !ids.length || ids.length < 1)
throw("Internal error: empty ID list?");
for (var i = 0; i < ids.length; ++i)
symbolTable[ids[i]] = null; // Declare, but do not initialize
},
总结
这里的bnf仅是有限的一些定义,尚且如此复杂,如果加上class、function、引用、指针、注释等等等等,一门语言的编译是得多么的复杂啊
2. 自创一门编程语言的14步
列了个大纲
3. 用LLVM开发新语言
为了让这份教程尽量易于理解、易于入手,我们决定不采用任何词法分析器和语法分析器的生成器[1],而是直接用C++实现所有功能。当然,LLVM与这些工具配合使用也完全没问题,如果愿意,你大可放手一试。
原来还有生成器,例如lex/yacc或flex/bison,不过找了下资料还是不懂这些东西是怎么用的!!
没有bnf,这个文章看的更懵逼
词法分析
要实现一门语言,第一要务就是要能够处理文本文件,搞明白其中究竟写了些什么。传统上,我们会先利用“词法分析器”(也称为“扫描器”)将输入切成“语元(token)”,然后再做处理。
问:第1篇参考文章的demo,好像没有词法分析器,直接将程序代码和bnf传入parse_string函数,得到ast。那么词法分析器是干啥的呢? 答:其实都是有词法分析的,另外,词法分析器,不是将代码全部分析完,再交给语法分析器,是语法分析器,一个一个逐步调用词法分析器的token,即给一个token,分析一下,应该放在ast树的哪个位置。
static int gettok() {
static int LastChar = ' ';
// Skip any whitespace.
while (isspace(LastChar))
LastChar = getchar();
if (isalpha(LastChar)) { // identifier: [a-zA-Z][a-zA-Z0-9]*
IdentifierStr = LastChar;
while (isalnum((LastChar = getchar())))
IdentifierStr += LastChar;
if (IdentifierStr == "def") return tok_def;
if (IdentifierStr == "extern") return tok_extern;
return tok_identifier;
}
if (isdigit(LastChar) || LastChar == '.') { // Number: [0-9.]+
std::string NumStr;
do {
NumStr += LastChar;
LastChar = getchar();
} while (isdigit(LastChar) || LastChar == '.');
NumVal = strtod(NumStr.c_str(), 0);
return tok_number;
}
if (LastChar == '#') {
// Comment until end of line.
do LastChar = getchar();
while (LastChar != EOF && LastChar != '\n' && LastChar != '\r');
if (LastChar != EOF)
return gettok();
}
// Check for end of file. Don't eat the EOF.
if (LastChar == EOF)
return tok_eof;
// Otherwise, just return the character as its ascii value.
int ThisChar = LastChar;
LastChar = getchar();
return ThisChar;
}
词法分析的过程很简单,根据输入用条件判断,将输入切割成一个一个token
语法分析
几乎又都是递归。记得之前看过,千万不要用人脑去递归,人不是计算机,不能开那么多栈,了解和设计递归的单元即可。
1. 设计了如下语法分析类,每个类代表一个特定的语法,最终组成ast:
ExprAST |–NumberExprAST |–VariableExprAST |–CallExprAST |–BinaryExprAST
FunctionAST PrototypeAST
```c
class PrototypeAST {
std::string Name;
std::vector<std::string> Args;
public:
PrototypeAST(const std::string &name, const std::vector<std::string> &args)
: Name(name), Args(args) {}
};
/// FunctionAST - This class represents a function definition itself.
class FunctionAST {
PrototypeAST *Proto;
ExprAST *Body;
public:
FunctionAST(PrototypeAST *proto, ExprAST *body)
: Proto(proto), Body(body) {}
};
其中,FunctionAST的参数是PrototypeAST和body,表示函数定义和函数体。PrototypeAST的参数是那么和args,表示函数名和参数列表。原来是这样做的,很棒哦。
ps: FunctionAST和CallExprAST是什么关系?一个是定义函数的AST,一个是函数调用的AST。
- 语法分析的过程,则是通过非常多的Parse函数实现,通过词法分析器得到的token,根据类型通过Parse函数,得到各自所属的AST。
比如ParseNumberExpr
static ExprAST *ParseNumberExpr() {
ExprAST *Result = new NumberExprAST(NumVal);
getNextToken(); // consume the number
return Result;
}
- 这里有几处很有意思,其中最显著的便是该函数的行为,它不仅消化了所有与当前生成规则相关的所有语元,还把下一个待解析的语元放进了词法分析器的语元缓冲(该语元与当前的生成规则无关)。这是非常标准的递归下降解析器的行为。
/// numberexpr ::= number
static ExprAST *ParseNumberExpr() {
ExprAST *Result = new NumberExprAST(NumVal);
getNextToken(); // consume the number
return Result;
}
/// parenexpr ::= '(' expression ')'
static ExprAST *ParseParenExpr() {
getNextToken(); // eat (.
ExprAST *V = ParseExpression();
if (!V) return 0;
if (CurTok != ')')
return Error("expected ')'");
getNextToken(); // eat ).
return V;
}
怎么理解呢?先看ParseParenExpr,这是解析括号的,因为进入ParseParenExpr函数,肯定当前的token是(,而括号在解析完生成语法树后没有价值,所以这里第一步,getNextToken(); // eat (.
,即进来直接放弃(,取下一个token,然后调用ParseExpression,因为这里的括号里面肯定是表达式(进入这个函数的条件,确定了这里不是函数),解析完表达式之后,再检查后面是不是),是的话说明语法ok,吃掉),不是的话,报错。
- 运算符优先级解析,处理比如“x+yz”,语法解析器既可以将之解析为“(x+y)z”,还是将之解析为“x+(y*z)“
LLVM IR(intermediate representation)
问:为什么要生成IR代码,语法分析的后续是什么? 答:IR是中间代码,按照LLVM的架构,任何语言的前端编译得到的AST,都将转为IR,IR经过优化后,再后端生成各cpu对应的目标代码,IR是LLVM的核心。
生成IR代码并不复杂,几行就搞定了,即每个AST节点,对应一个codegen方法,通过LLVM IR提供的工具类,生成IR代码。
Adding JIT and Optimizer Support
原来编译器做了constant folding优化,以后可以大胆的写类似的代码了:10+10+23+15
Just-In-Time Compiler,中文意思是即时编译器
Extending the Language: Control Flow
添加if和for
SSA和PHI的概念没怎么搞懂
Extending the Language: User-defined Operators
自定义一元运算符,二元运算符
Extending the Language: Mutable Variables
实现变量编译 var i = 0
Conclusion and other useful LLVM tidbits
4. 90分钟实现一门编程语言——极简解释器教程
LLVM每日谈
专门讲LLVM的