作者: SilverFruity, https://github.com/SilverFruity
小编寄语:
上一周我们发布了《OCRunner 第零篇:从零教你写一个 iOS 热修复框架》。很多读者对这个方案是否能上架特别感兴趣。在这里统一做一下回复:
根据作者描述,这个框架没有调用任何私有 API,目前是可以上架的。
当然,我们也觉得做技术的要对一些新鲜事物带有一棵好奇之心,搞清楚其中原理比这个东西是否能上线比更重要。
至于怎么上线以及是否符合苹果规范就看各家产品决策了。审核本身是一个博弈的过程,毕竟像 RN、Weex 这类的都是可以动态下发代码的,现在有很多产品可以上线。但是像 JSPatch 却又被定义成有安全风险不让上线。
相信看完今天这篇文章的朋友,应该能理解为什么这个方案可以上线。尤其是误以为 OCRunner 是基于 JSCore 一定要花时间好好了解一下哦。
在正文开始前,我们先看看给出的语法树解释执行.gif,OCRunner的核心解释执行部分也是如此。
如果你让我仅仅只靠文字就能给你解释清楚 OCRunner 是怎么运行起来的,你们可能是在为难我小蒋,肚子里墨水真的没那么多啊,太难了????。所以在这一小节,我给大家准备了一个简单版 OCRunner,希望大家能从这个例子中,真正知道它是如何运行的。
希望你们看见项目中的 SingleEngine 类不要笑(哈哈哈),我只是想表达 单缸发动机 的意思 - _ -,我心爱的小摩托就是单缸拖拉机。
本节假设读者并不具备编译相关的知识。
本文对 lex 和 yacc 介绍有限,关于 flex 和 bison 的详细使用可以参照《flex与bison中文版》或者 《flex&bison》。
SimpleOCRunnerDemo[1],建议使用 git clone
的形式下载源码,每个小节的相应源码都对应在各自的 commit 里。通过边看文章边阅读源码的形式食用,更香哟。
我们将通过以下这个例子,来说明 OCRunner 执行的主要实现,将省略补丁文件的序列化和加载过程。
现在,我们的目标是让这段文本像 C 一样执行,并且最终变量 c 的值为 5 。
-
int d =
2;
-
int add(
int arg0, double arg1){
-
return arg0 + arg1 + d;
-
}
-
int a =
1;
-
double b =
2.0;
-
int c = add(a, b);
词法分析器
通俗来说,词法分析就是将符合相应规则的字符串转换为一个 Token(可以将它看为一个由类型和值两个属性组成的类)。
比如当匹配到 'int' 时,'int' 将会被转换为 INT_TYPE ,也是就是 Token 的类型,它的值可以是我们后续的值类型(一个枚举)。
再比如当匹配到 '1.0' 字符串时,'1.0' 被转换为 DOUBLE_LITERAL 类型并且 Token 此时的值为浮点数 1.0。
针对这里例子,我们现在有如下几个规则:
关键词: int、double、return
标识符(可以是变量名、类型等): 以字母、_ 、$ 开头的字符串
数值: 整数、浮点数
符号: ( ) ; { } , = +
上述都是我们接下来需要识别的,我们将直接使用 flex 完成词法分析器。
词法分析器 lexer.l 中的代码:
-
%option noyywrap
-
%{
-
#
import <Foundation/Foundation.h>
-
#include
"y.tab.h"
-
%}
-
%%
-
"," {
return
',';}
-
";" {
return
';'; }
-
"(" {
return
'('; }
-
")" {
return
')'; }
-
"{" {
return
'{'; }
-
"}" {
return
'}'; }
-
"=" {
return
'='; }
-
"+" {
return
'+'; }
-
"-" {
return
'-'; }
-
"int" {
return INT_TYPE; }
-
"double" {
return DOUBLE_TYPE; }
-
"return" {
return RETURN; }
-
// 浮点数
-
[
0
-9]+\.[
0
-9]+ { yylval.doubleValue = atof(yytext);
return DOUBLE_LITERAL; }
-
// 整数
-
[
1
-9][
0
-9]* { yylval.intValue = (
int)strtol(yytext, NULL,
10);
return INTETER_LITERAL; }
-
// 标识符
-
[A-Za-z_$][A-Za-z_$
0
-9]* { yylval.stringValue = yytext;
return IDENTIFIER; }
-
%%
Flex语法简单简介
上半部分的
%{ %}
主要用于书写 C 代码,引入头文件,自定义函数等等,最终会以文本的形式被添加到 lexer.yy.c 中( Demo 中你可以在编译缓存中找到该文件)。下半部分的
%% %%
就是关键了,通过使用正则表达式,将符合规则的字符流转换为相应的 token 。yylval这则是 Token 的值,默认情况下它一个 int 类型,但是在 bison 中可以使用 %union 声明来重新定义它的类型。
当我们调用词法分析器后,上述文本将会做出如下转换,如何对它进行语法匹配,这就是接下来的内容了。
-
int d =
2 ;
-
> INT_TYPE IDENTIFIER = INTETER_LITERAL ;
-
int add (
int arg0 , double arg1 ) {
-
> INT_TYPE IDENTIFIER ( INT_TYPE IDENTIFIER , DOUBLE_TYPE IDENTIFIER ) {
-
return arg0 + arg1 + d ;
-
> RETURN IDENTIFIER + IDENTIFIER + IDENTIFIER ;
-
}
-
> }
-
int a =
1 ;
-
> INT_TYPE IDENTIFIER = INTETER_LITERAL ;
-
double b =
2.0 ;
-
> DOUBLE_TYPE IDENTIFIER = INTETER_LITERAL ;
-
int c = add ( a , b ) ;
-
> INT_TYPE IDENTIFIER = IDENTIFIER ( IDENTIFIER , IDENTIFIER ) ;
语法分析器
语法分析器 parser.y 中的代码,目前还没有任何语法相关的逻辑。
-
%{
-
#
import <Foundation/Foundation.h>
-
#define YYDEBUG
1
-
#define YYERROR_VERBOSE
-
extern
int yylex (void);
-
extern void yyerror(
const char *s);
-
%}
-
%union{
-
uint64_t intValue;
-
double doubleValue;
-
char *stringValue;
-
__unsafe_unretained id object;
-
}
-
%token<stringValue> IDENTIFIER
-
%token<doubleValue> DOUBLE_LITERAL
-
%token<intValue> INTETER_LITERAL
-
%token INT_TYPE
-
%token DOUBLE_TYPE
-
%token RETURN
-
%start door
-
%
type<object> statements
-
-
%%
-
door:
-
/*empty*/
-
| statements;
-
;
-
statements:
-
/*empty*/
-
;
-
%%
-
-
void yyerror(
const char *s){
-
}
Yacc语法简单介绍
%{%}
中和 Flex 是一样的。但是关于这里的 yylex 和 yyerror 两个函数,他们两兄弟是必须引入的%union 如之前所说,将重新定义 yylval 的类型,可以在词法分析阶段,保留不同类型的值
%token<stringValue> IDENTIFIER 将在 yacc 声明一个名为 IDENTIFIER 的 token,并且表明了它的值类型(获取它的值时,将从 union 的哪个字段取值)。
%type<object> statements 与 %token 不同是的,这是声明一条规约(语法规则)。
%start 将从指定的规约,作为语法解析器的入口
关于
%{%}
和%% %%
,与 flex 不同是的,%% %%
中采用 BNF 文法的变种来描述语法规则,当你明白了它的运作方式,其实可读性还是很高的。现在我们需要使用 lexer.l 以及 parser.y ,完成对
a = 1 + 2; b = 2.0 - 1.0;
的识别,并且打印计算结果。使用 Yacc 实现对两个表达式的识别以及计算的源码,已经在仓库 SimpleOCRunnerDemo[2] 的 不同类型简单计算的例子[3] 中完成,对 Yacc 语法不熟悉的小伙伴,可以看完这一小部分,再去看源码。为限制本文篇幅,我将尽量减少在文中的代码。
首先我们先将
a = 1 + 2; b = 2.0 - 1.0;
转换为 Token :-
//表达式 1
-
IDENTIFIER = INTETER_LITERAL + INTETER_LITERAL ;
-
//表达式 2
-
IDENTIFIER = DOUBLE_LITERAL - DOUBLE_LITERAL ;
通俗来说,如果我们逐个逐个的读入每个 Token,我们要匹配上述两个表达式,可以有以下代码逻辑:
-
extern Token nextToken(void);
-
while(token = nextToken()):
-
if token == IDENTIFIER:
-
if nextToken() == '=':
-
token = nextToken();
-
if token == INTETER_LITERAL:
-
if nextToken() == '+':
-
if nextToken() == INTETER_LITERAL:
-
//完成 IDENTIFIER = INTETER_LITERAL + INTETER_LITERAL ;
-
return
-
if token == DOUBLE_LITERAL:
-
if nextToken() == '-':
-
if nextToken() == DOUBLE_LITERAL:
-
//完成 IDENTIFIER = DOUBLE_LITERAL - DOUBLE_LITERAL ;
-
return
-
break;
-
-
// 这个逻辑使用 Yacc 代码可以表示为
-
assign_expression:
-
IDENTIFIER '=' expression
-
;
-
expression:
-
INTETER_LITERAL + INTETER_LITERAL
-
| DOUBLE_LITERAL - DOUBLE_LITERAL
-
;
-
// NAME: 其实是实现了一条规约(语法规则),始终对下一个Token进行匹配,其实中的 | 表示一个 or 的逻辑分支,还有就是规约可以递归使用。更多的Yacc语法可以参阅《flex与bison中文版》53页。
-
现在让我们回到初始目标,现在我们要实现它相应的语法分析器:
-
int d =
2;
-
int add(
int arg0, double arg1){
-
return arg0 + arg1 + d;
-
}
-
int a =
1;
-
double b =
2.0;
-
int c = add(a, b);
还记得我们在词法分析器中的实现吗,文本到 Token 的转换规则:
-
int -> INT_TYPE
-
double -> DOUBLE_TYPE
-
'a-zA-z' -> IDENTIFIER
-
1 -> INTEGER_LITERAL
-
2.0 -> DOUBLE_LITERAL
-
(){}+,=; -> (){}+,=;
//字符
首先,我们完全可以确定的是,出现最多的 Token 组合为:
-
// int/double xxx
-
TYPE IDENTIFIER
同时我们的数据类型有两种,分别为 int / double ,因此我们的第一条规约为:
-
specifier_type:
-
INT_TYPE
-
|DOUBLE_TYPE
-
;
接下来,我们来梳理一下所有的声明语法(变量、函数):
-
int a
-
double b =
2.0
-
int add(
int arg0,
int arg1)
针对以上三个声明语法,可以使用以下两个规约匹配:
-
// 对int/double xxx统一处理,其他的单独处理即可
-
declaration:
-
specifier_type IDENTIFIER
-
| declaration
'=' expression
-
| declaration
'(' declaration_list
')'
-
;
-
-
// declaration_list 就是一个以','为分隔符的列表处理
-
declaration_list:
-
declaration
-
| declaration_list
',' declaration
-
;
接下来我们需要处理表达式,我们这里对表达式简单的定义为:'=' 和 'return' 右部分,不包含 ';' 。
-
1
-
1.0
-
a
-
a + b
-
add(a, b)
针对以上表达式,可以使用的最简规约
-
// 处理 1、1.0、a
-
primary_expression:
-
IDENTIFIER
-
| INTETER_LITERAL
-
| DOUBLE_LITERAL
-
;
-
-
// 处理 a + b、add(a, b)
-
expression:
-
primary_expression
-
| expression
'+' expression
-
| expression
'(' expression_list
')'
-
;
-
// expression_list与declaration_list雷同
实现了expression规约后,实现 return 就很简单了 RETURN expression
。
我们将 { ... }
这个整体统称为一个块或者是作用域,它由表达式的列表或者子作用域组成。
在完成对块的匹配之前,我们首先要能够匹配多个不同的声明。
根据已经实现的规约,在其尾部加一个;
,完成一个声明的匹配。
因此实现了 statement
规约,用以将各个声明的规约整合。
-
statement:
-
declaration
';'
-
| expression
';'
-
| RETURN expression
';'
-
;
其次,我们还需要匹配作用域中的多个声明或者零个声明,那么 statement_list
应运而生。
-
statement_list:
-
| statement
-
| statement_list statement
-
;
这个时候,我们已经能够处理作用域中的声明列表了,那么针对作用域的规约如下:
-
scope_statements:
-
'{' statement_list
'}'
-
;
目前就只有对函数的声明没有实现了,但我们早已在 declaration
中实现了对了 int add(int a, double b)
识别。最终的 statement
规约如下:
-
statement:
-
declaration
';'
-
| expression
';'
-
| RETURN expression
';'
-
//函数声明的识别
-
| declaration scope_statements
-
;
本小节相关代码,在SimpleOCRunnerDemo[4] 的 p 语法分析[5] 提交中。
生成语法树
在我们这个例子中,语法树中需要的节点如下(如果小伙伴想添加更多的语法,可自行更改):
所有节点都继承自ASTNode类。
ASTSpecifierNode变量类型节点:
int
,double
ASTDeclareNode变量声明节点:
int a
ASTValueNode值节点:
2
,1.0
,a
ASTInitDeclareNode变量初始化声明节点:
double b = 2.0
ASTBinaryNode二元运算节点:
a + b
ASTScopeNode作用域节点:
{}
ASTFunctionDeclareNode函数声明节点:
int add(int arg0, double arg1)
ASTFunctionImpNode函数节点:
int add(int arg0, double arg1) { }
**ASTReturnNode **return节点:
return xxx
ASTFunctionCallNode函数调用节点:
add(a, b)
ASTSpecifierNode int/double
:我们这里其实就是一个简单枚举,但我们还添加了类型名称的属性
-
typedef enum{
-
ASTSpecifierTypeInt,
-
ASTSpecifierTypeDouble
-
}ASTSpecifierType;
-
-
@
interface ASTSpecifierNode: ASTNode
-
@property (nonatomic, assign) ASTSpecifierType
type;
-
@property (nonatomic, strong) NSString *typeName;
-
@end
ASTDeclareNode int a
: 可以看出由一个变量类型( ASTSpecifierNode )和变量名组成
-
@
interface ASTDeclareNode: ASTNode
-
@property (nonatomic, strong) ASTSpecifierNode *
type;
-
@property (nonatomic, strong) NSString *varname;
-
@end
ASTValueNode 2
, 1.0
, a
: 有三种值,整型、浮点数、变量名
在我们的代码中存在两种变量,标识符:a
,标量:1.0 ...
,同时一个变量只能拥有一个值和一个值类型,所以我们使用enum ASTValueNodeType
+ union ASTValueNodeValue
的形式来完成类型和值的对应。
-
typedef enum{
-
ASTValueNodeIdentifier,
-
ASTValueNodeInt,
-
ASTValueNodeDouble
-
}ASTValueNodeType;
-
-
typedef union {
-
void *pointerValue;
-
int64_t intValue;
-
double doubleValue;
-
}ASTValueNodeValue;
-
-
@
interface ASTValueNode: ASTNode
-
@property (nonatomic, assign) ASTValueNodeType
type;
-
@property (nonatomic, assign) ASTValueNodeValue value;
-
@end
ASTInitDeclareNode double b = 2.0
: 此时我们再处理初始化声明表达式的数据结构就很简单了,由变量声明节点和值节点组成,结构如下
-
@
interface ASTInitDeclareNode: ASTNode
-
@property (nonatomic, strong) ASTDeclareNode *declare;
-
@property (nonatomic, strong) ASTNode *expression;
-
@end
ASTBinaryNode a + b
: 主要有左右两个表达式以及中间的操作符组成
事实上,左右的表达式可能并不单单只是一个变量名(或者表达式),因此,我们这里使用的是ASTNode
-
typedef enum {
-
ASTBinaryNodeOperatorAdd
-
}ASTBinaryNodeOperator;
-
-
@
interface ASTBinaryNode: ASTNode
-
@property (nonatomic, assign) ASTBinaryNodeOperator operator;
-
@property (nonatomic, strong) ASTNode *left;
-
@property (nonatomic, strong) ASTNode *right;
-
@end
ASTScopeNode {}
中主要存在的是多个语法节点,因此,其属性只有一个存储节点的数组
ASTFunctionDeclareNode int add(int a, double b)
,主要由一个 ASTDeclareNode(int add
)和它的数组(int a, double b
)组成
ASTFunctionImpNode int add(int a, double b) { }
,则是由上述两个节点组成
ASTFunctionCallNode add(a, b)
,由一个调用者节点和参数节点数组组成
因此他们的结构如下:
-
@
interface ASTScopeNode : ASTNode
-
@property (nonatomic, strong) NSMutableArray *nodes;
-
@end
-
-
@
interface ASTFunctionDeclareNode: ASTNode
-
@property (nonatomic, strong) ASTDeclareNode *declare;
-
@property (nonatomic, strong) NSMutableArray *argDeclares;
-
@end
-
-
@
interface ASTFunctionImpNode: ASTNode
-
@property (nonatomic, strong) ASTFunctionDeclareNode *declare;
-
@property (nonatomic, strong) ASTScopeNode *scope;
-
@end
-
-
@
interface ASTFunctionCallNode: ASTNode
-
@property (nonatomic, strong) ASTNode *caller;
-
@property (nonatomic, strong) NSMutableArray *args;
-
@end
最终 parser.y 中的部分代码如下,globalAst 为一个全局的语法树变量:
-
door:
-
statement_list
-
{
-
globalAst.nodes = $
1;
-
}
-
;
-
statement_list:
-
{
-
__autoreleasing id value = [NSMutableArray array];
-
$$ = value;
-
}
-
| statement_list statement
-
{
-
[$
1 addObject: $
2];
-
$$ = $
1;
-
}
-
;
-
.......
关于在 .l 和 .y中书写代码不方便的问题,可以在 vscode 中安装 flex 和 bsion 的相关插件,有了代码提示,体验会好很多很多。
单元测试
为什么会在这儿提到单元测试呢?
oc2mango 以及 OCRunner 能够开发出来,单元测试有超大功劳。
针对 oc2mango 方面,随着项目的发展,越来越多的语法需要支持,当你在修改语法分析器的时候,稍有一个不慎,就极有可能导致已经支持的语法变成不支持,没有单元测试的话,后续的开发几乎就寸步难行了。现在反思一下,每留下一个爆红的测试示例,有时也像一个一个挑战,等着你去征服他们,绿了以后感觉贼爽。
之前我们一直没有提到单元测试,但在目前这个阶段,我们已经可以愉快的使用单元测试了(其实我们在最开始的时候就应该使用的????,篇幅有限啊)。
目前我们的单元测试目标很明确:检验生成的语法树是否正确。
那我们的输入->输出的概念应该为:源码->语法树->单元测试->是否正确。
之前的代码,我们是无法通过 [SingleEngine run:] 得到语法树的,因此我们在SingleEngine中添加了一个 + (AST *)parse: (NSString *)source的类方法,用于完成:源码->语法树。
为了方便单元测试,也在 SingleEngine 中特意添加了一个 parserError 用于最基础的语法测试判断。
当你打算尝试加入一个新的语法时,你可以写一个最简单的关于 parserError 的单元测试,亮一个红灯。
示例:SimpleOCRunnerDemoTests.m 中的 testExample 代码:
-
- (void)testExample {
-
NSString *source =
-
@
"int a = 1;"
-
"double b = 1.0;";
-
AST *ast = [SingleEngine parse:source];
-
XCTAssert(parserError ==
nil, @
"error: %@", parserError);
-
ASTInitDeclareNode *node1 = (ASTInitDeclareNode *)ast.nodes.firstObject;
-
XCTAssert([node1 isKindOfClass:[ASTInitDeclareNode class]]);
-
XCTAssert([node1.declare.varname isEqual:@
"a"]);
-
XCTAssert(node1.declare.
type !=
nil);
-
XCTAssert(node1.declare.
type.
type == ASTSpecifierTypeInt);
-
XCTAssert([node1.expression isKindOfClass:[ASTValueNode class]]);
-
XCTAssert([(ASTValueNode *)node1.expression value].intValue ==
1);
-
-
ASTInitDeclareNode *node2 = (ASTInitDeclareNode *)ast.nodes.lastObject;
-
XCTAssert([node2 isKindOfClass:[ASTInitDeclareNode class]]);
-
XCTAssert([node2.declare.varname isEqual:@
"b"]);
-
XCTAssert(node2.declare.
type !=
nil);
-
XCTAssert(node2.declare.
type.
type == ASTSpecifierTypeDouble);
-
XCTAssert([node2.expression isKindOfClass:[ASTValueNode class]]);
-
XCTAssert([(ASTValueNode *)node2.expression value].doubleValue ==
1.0);
-
}
本小节相关代码位于 Section: 生成语法树[6].
执行语法树
-
int d =
2;
-
int add(
int arg0, double arg1){
-
return arg0 + arg1 + d;
-
}
-
int a =
1;
-
double b =
2.0;
-
int c = add(a, b);
在开始正文以前,我们先思考几个问题:
int a = 1;
,关于 a 的值,应如何存储,并且在后续的计算中还能获取到 a 的值?int a = 1; double b = 1.0; int c = a + b;
,关于这里,在计算 c 时,a 和 b 的值,如何才能获取到,并且 a、b 的值是两个不同的类型,如何让他们以自己的类型进行计算,并且最终的值被强转为int?add(a , b);
函数调用时,参数如何传递?在
add
函数的调用中,d 的值如何获取?如何兼容的 Objective-C 的 TypeEncode[7]?
现在有几个概念需要讲一下了。
作用域(EvalScope)
{ }
就可视为一个作用域,EvalScope 主要由一个 NSMuatbleDictionary 和一个父作用域的引用组成。
它的作用主要是:
1.存储在当前作用域中声明的变量的值,比如: int a = 1
,EvalScope["a"] = ORValue(1)。当 int b = a
这种情况时,则会去 EvalScope 查找 'a' 变量的值,然后再注册一个 'b' 的变量。
2.父作用域的引用主要用于跨域操作时,跨域变量查找以及修改。我们的例子中,也出现了这种情况。
中间值(ORValue)
站在作用域的基础之上,在字典中保存的值,需要是一个对象。
尽管目前我们只需要处理 int 和 double 类型的值,但随着类型的增加,需要一个类来封装类型处理的相关逻辑以及值在内存中的存储的。
为了兼容 Objective-C 的 TypeEncode,在类中,添加一个 typeEncode 的属性,以做到:ORValue = 值 + TypeEncode 的组合。
同时,为了能够控制当前的执行状态,拥有一个 controlState 属性,用于在代码中实现 return 、continue、break。
全局函数表(FunctionTable)
为了全局函数能够正常调用,比如通过 add(1, 0)
的形式,即可调用add函数的实现。因此我们实现了一个单例主要用于存储 [String : ASTFunctionImpNode] 的字典。
关于 ASTFunctionImpNode 的注册和执行的问题:
当遍历 execute 语法树时,此时为第一次调用 ASTFunctionImpNode.execute ,这个时候会向全局函数表进行注册,此时 EvalArgsStack 中的元素的个数必定为0。所有的函数和方法调用的时候,此时 EvalArgsStack 的元素的个数必定 >= 1,因此除了第一次注册执行以外,此后 ASTFunctionImpNode 所有的 execute,都进行解释执行操作。
函数参数栈(EvalArgsStack)
它其实很简单。在函数调用前,先将所有的参数按照顺序组成一个数组压入栈中,函数执行时,从栈顶取出数组,随后注册到函数作用域中,当函数执行完成后,再将参数数组出栈。
当函数执行时,是如何获取到他们的值的呢?
-
//我们再回顾一下 函数声明的结构
-
@
interface ASTFunctionDeclareNode: ASTNode
-
@property (nonatomic, strong) ASTDeclareNode *declare;
-
@property (nonatomic, strong) NSMutableArray <ASTDeclareNode *>*argDeclares;
-
@end
已知 int add(int arg0, double arg1){ }
我们调用add(1, 0);
。
首先,我们使用 'add' 在 FunctionTable 中查找到 add
函数的实现(ASTFunctionImpNode)。
调用函数实现前,先执行[EvalArgsStack.push:@[ORValue(1), ORValue(0)]]
。
最终 ASTFunctionImpNode.exexcute 时,向作用域中注册参数变量的逻辑如下:
-
NSArray *args = [EvalArgsStack top];
-
NSArray *declares = self.declare.argDeclares;
-
functionSubScope[declares[
0].varname] = args[
0];
//functionSubScope["arg0"] = args[0];
-
functionSubScope[declares[
1].varname] = args[
1];
//functionSubScope["arg1"] = args[1];
关于最终代码运行的逻辑如下:
-
int d =
2;
// TopScope[“d”] = ORValue(2);
-
int add(
int arg0, double arg1){
-
// ASTFunctionImpNode.exexcute(SubScope) -> ORValue:
-
// list<ORValue> = ArgsStack.last
-
// SubScope[“arg0”] = list[0] = ORValue(1)
-
// SubScope[“arg1”] = lsit[1] = ORValue(2.0)
-
// d = SubScope->superScope[“d”] = TopScope[“d”] = ORValue(2)
-
// return ORValue(1) + ORValue(2.0) + ORValue(2)
-
return arg0 + arg1 + d;
-
}
-
int a =
1;
// TopScope[“a”] = ORValue(1);
-
double b =
2.0;
// TopScope[“b”] = ORValue(2.0);
-
int c = add(a, b);
-
// ASTFunctionImpNode = GlobalFunctionTable[“add”]
-
// ArgsStack.push([a, b])
-
// SubScope->superScope = TopScope
-
// TopScope[“c”] = ASTFunctionImpNode.exexcute(SubScope)
-
// ArgsStack.pop()
本小节相关代码位于Section: 执行语法树[8].
在这个简单的 demo 里面,许多功能都还未完成:操作符、基本类型转换、指针、方法替换、控制语句、结构体等等,但我希望能够让小伙伴们对语法树解释执行有一个基本的了解。针对未实现的功能,想深入的人,完全可以通过修改 demo,完成以上功能,疑惑的地方,可以查看 OCRunner 的源码。
One More Thing
这是一个系列文章,感兴趣的可以关注一下「老司机技术周报」公众号,后续文章都会在公众号中发布。感兴趣的也可以看一下之前已经发布的文章:
OCRunner 第零篇:从零教你写一个 iOS 热修复框架
关注我们
我们是「老司机技术周报」,每周会发布一份关于 iOS 的周报,也会定期分享一些和 iOS 相关的技术。欢迎关注。
关注有礼,关注【老司机技术周报】,回复「2020」,领取学习大礼包。
后续规划
OCRunner 第二篇:二进制补丁文件的实现
以上,作者正在快马加鞭打磨中.....
参考资料
[1]
SimpleOCRunnerDemo: https://github.com/SilverFruity/SimpleOCRunnerDemo
[2]SimpleOCRunnerDemo: https://github.com/SilverFruity/SimpleOCRunnerDemo
[3]不同类型简单计算的例子: https://github.com/SilverFruity/SimpleOCRunnerDemo/commit/c09d35868207ec44e7a4854552e9b19246f561d1
[4]SimpleOCRunnerDemo: https://github.com/SilverFruity/SimpleOCRunnerDemo
[5]p 语法分析: https://github.com/SilverFruity/SimpleOCRunnerDemo/commit/850de3fe2db9ce3a059e809560bb2fb88ea3ac06
[6]Section: 生成语法树: https://github.com/SilverFruity/SimpleOCRunnerDemo/commit/4f27bd65488a2cf9b669b1047ab242b2f8a5999d
[7]TypeEncode: https://developer.apple.com/library/archive/documentation/Cocoa/Conceptual/ObjCRuntimeGuide/Articles/ocrtTypeEncodings.html
[8]Section: 执行语法树: https://github.com/SilverFruity/SimpleOCRunnerDemo/commit/33e2358c4e06baa1f35d4309b76f376a753f58fc
转载:https://blog.csdn.net/sinat_35969632/article/details/113287574