魏名华

不要偷懒,做更好的自己

Nothing


No Welcome Message

iOS热更新-编译

安装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。

  1. 语法分析的过程,则是通过非常多的Parse函数实现,通过词法分析器得到的token,根据类型通过Parse函数,得到各自所属的AST。

比如ParseNumberExpr

static ExprAST *ParseNumberExpr() {
    ExprAST *Result = new NumberExprAST(NumVal);
    getNextToken(); // consume the number
    return Result;
}
  1. 这里有几处很有意思,其中最显著的便是该函数的行为,它不仅消化了所有与当前生成规则相关的所有语元,还把下一个待解析的语元放进了词法分析器的语元缓冲(该语元与当前的生成规则无关)。这是非常标准的递归下降解析器的行为。
/// 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,吃掉),不是的话,报错。

  1. 运算符优先级解析,处理比如“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的