430 lines
22 KiB
Markdown
430 lines
22 KiB
Markdown
# 28|增加更丰富的类型第3步:支持数组
|
||
|
||
你好,我是宫文学。
|
||
|
||
前面我们已经给我们的语言,增加了两种数据类型:浮点数和字符串。那么,今天这一节课,我们再继续增加一种典型的数据类型:数组。
|
||
|
||
数组也是计算机语言中最重要的基础类型之一。像C、Java和JavaScript等各种语言,都提供了对数组的原生支持。
|
||
|
||
数组跟我们之前已经实现过的number、string类型的数据类型相比,有明显的差异。所以在这里,你也能学到一些新的知识点,包括,如何对数组做类型处理、如何设计数组的内存布局,如何正确访问数组元素,等等。
|
||
|
||
在完成这节课的任务后,我们的语言将支持在数组中保存number和string类型的数据,甚至还可以支持多维数组。是不是感觉很强大呢?那就赶紧动手试一试吧!
|
||
|
||
那么这实现的第一步,我们需要修改编译器前端的代码,来支持与数组处理有关的语法。
|
||
|
||
## 修改编译器前端
|
||
|
||
在编译器前端,我们首先要增加与数组有关的语法规则。要增加哪些语法规则呢?我们来看看一个最常见的使用数组的例子中,都会涉及哪些语法特性。
|
||
|
||
```plain
|
||
let names:string[] = ["richard", "sam", "john"];
|
||
let ages:number[] = [8, 18, 28];
|
||
let a2:number[][] = [[1,2,3],[4,5]];
|
||
for (let i = 0; i< names.length; i++){
|
||
println(names[i]);
|
||
}
|
||
|
||
```
|
||
|
||
在这个例子中,我们首先声明了一个字符串类型的数组,然后用一个数组字面量来初始化它。你还可以用同样的方法,声明并初始化一个number数组。最后,我们用names\[i\]这样的表达式来访问数组元素。
|
||
|
||
在这个例子中,你会发现三个与数组有关的语法现象,分别是**数组类型**、**数组字面量**和**下标表达式**。
|
||
|
||
首先是数组类型。在声明变量的时候,我们可以用string\[\]、number\[\]来表示一个数组类型。这个数组类型是一个基础类型再加上一对方括号\[\]。在这里,我们甚至还声明了一个二维数组。
|
||
|
||
所以,我们还需要扩展与类型有关的语法规则。你看看下面的语法规则,**你可以这样表示数组类型:“primaryType ‘\[’ ‘\]’”。而primaryType本身也可以是一个数组类型,这样就能表达多维数组了,比如a\[\]\[\]。**
|
||
|
||
```plain
|
||
primaryType : predefinedType | literal | typeReference | '(' type_ ')' | primaryType '[' ']' ;
|
||
|
||
```
|
||
|
||
不过,这是一个左递归的文法,就会遇到我们之前学过的左递归问题。我们可以改写一下,变成下面的文法:
|
||
|
||
```plain
|
||
primaryType : primaryTypeLeft ('[' ']')* ;
|
||
primaryTypeLeft : predefinedType | literal | typeReference | '(' type_ ')' | primaryType ;
|
||
|
||
```
|
||
|
||
这样的话,我们每次解析完毕一个primaryTypeLeft以后,再看看后面有没有跟着一对方括号就行了。如果出现多对方括号,就表示这是一个多维数组。
|
||
|
||
第二个语法现象是数组字面量,比如string字面量\[“richard”, “sam”, “john”\]和number字面量\[8, 18, 28\]。
|
||
|
||
这里,我们也总结出了数组字面量的语法规则:**每个数组字面量都是以方括号括起来的,方括号里面可以有一组元素,这些元素可以是一个表达式,或者是一个标识符,中间以逗号分割。**
|
||
|
||
```plain
|
||
primary: literal | functionCall | '(' expression ')' | typeOfExp | arrayLiteral;
|
||
arrayLiteral : ('[' elementList? ']');
|
||
elementList : arrayElement (','+ arrayElement)* ;
|
||
arrayElement : expression ','? ;
|
||
|
||
```
|
||
|
||
你要注意最后一条语法规则“arrayElement : expression ‘,’? ;”。这条语法规则的意思是,数组中的最后一个元素,后面仍然可以跟一个逗号。比如上面的number数组等价于\[8, 18, 28, \]。
|
||
|
||
第三个语法现象是下标表达式。我们可以用names\[i\]来引用数组的第i个元素。
|
||
|
||
所以,我们要扩展表达式的语法规则,**让基础式后面可以跟上一对方括号,变成一个下标表达式**。修改后的语法规则如下。这个文法的书写方式也同样避免了左递归。
|
||
|
||
```plain
|
||
expression: 'typeof' expression | assignment ('[' expression ']')* ;
|
||
|
||
```
|
||
|
||
上面就是目前我们需要扩展的语法规则了。为了支持新的语法规则,我们要创建几个新的AST节点对象:一个是ArrayPrimType,代表数组类型节点;第二个是ArrayLiteral,代表数组字面量;还有一个是IndexedExp,表示像a\[2\]这样的带有index的下标表达式。
|
||
|
||
在这里,你要注意,凡是表达式后面跟一个\[index\],都表示访问该表达式的第n个元素。这个表达式不一定是个数组,只要是能够用下标访问的对象就行。比如,你可以像下面的程序那样,用下标访问一个字符串中的字符。
|
||
|
||
```plain
|
||
let name = "Richard";
|
||
name[1]; //返回'i'
|
||
|
||
```
|
||
|
||
在补充了相应的AST节点以后,我们就可以根据语法规则修改语法解析程序了。你可以试着运行下面这个的测试程序,用“node play example.ts -v”命令,带上参数-v,打印出AST来看看。
|
||
|
||
```plain
|
||
let a: number[]; //数组类型
|
||
let b = 4;
|
||
a = [1, 2, 3+3, b]; //数组字面量,其中的元素可以是表达式
|
||
println(a[2]); //下标表达式
|
||
|
||
```
|
||
|
||
在终端输出的AST如下图所示,我在里面标注了上面讨论到的几种AST节点。
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/df/dc/dfccd7b741bc8020e376950238d047dc.png?wh=1404x1182)
|
||
|
||
你还可以看看语法分析程序对多维数组的支持情况,比如下面的示例程序:
|
||
|
||
```plain
|
||
let a2:number[][] = [[1,2,3],[4,5]]; //二维数组
|
||
println(a2);
|
||
println(a2[1]); //打印其中一个维度
|
||
println(a2[0][2]); //打印一个元素
|
||
|
||
```
|
||
|
||
它的AST结构如下图所示,你要注意,数组类型、数组字面量和数组元素的引用都是如何形成层层嵌套的结构的。
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/f6/dc/f64a681fae422196fc8cfbd4166132dc.png?wh=1078x1254)
|
||
|
||
除了语法方面的工作以外,我们语义分析方面也要做一些增强,包括**类型处理**和**左值分析**等方面的工作。
|
||
|
||
为了实现类型处理,我们首先需要添加新的ArrayType类型。ArrayType类型会引用一个基础类型。到现在为止,我们的类型体系越来越丰富了,我把原来的类型体系图更新了一版,你可以看一下:
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/cf/43/cf23228f09b4a9356dcb637de4a11643.png?wh=968x876)
|
||
|
||
然后,在类型处理的时候,你还需要完成这几项工作。
|
||
|
||
首先,我们要能够根据类型声明的AST节点,计算出变量的类型。你可以参考[TypeResolver](https://gitee.com/richard-gong/craft-a-language/blob/master/28/semantic.ts#L123)中的代码:
|
||
|
||
```plain
|
||
visitArrayPrimTypeExp(te:ArrayPrimTypeExp):any{
|
||
//求出基础类型
|
||
let t = this.visit(te.primType) as Type;
|
||
|
||
//创建ArrayType
|
||
let t1 = new ArrayType(t);
|
||
|
||
return t1;
|
||
}
|
||
|
||
```
|
||
|
||
第二,要计算出数组字面量的类型,并检查类型是否一致。比如,下面的示例程序中,数组字面量的类型是stirng\[\],而变量a3的类型是number\[\],所以会检查出类型错误出来。
|
||
|
||
```plain
|
||
let a3:number[] = ["Hello", "PlayScript"];
|
||
|
||
```
|
||
|
||
并且,如果数组字面量中的个别元素跟类型声明不一致,也需要能够检查出来:
|
||
|
||
```plain
|
||
let a4:number[] = [2, "PlayScript"];
|
||
|
||
```
|
||
|
||
具体实现你可以参考[TypeChecker](https://gitee.com/richard-gong/craft-a-language/blob/master/28/semantic.ts#L512)中的[visitArrayLiteral](https://gitee.com/richard-gong/craft-a-language/blob/master/28/semantic.ts#L1062)和[visitBinary](https://gitee.com/richard-gong/craft-a-language/blob/master/28/semantic.ts#L715)方法。代码有点多,我就不贴在文稿里了,你可以点击链接查看。
|
||
|
||
第三,我们还要能够根据下标表达式,计算每个表达式节点的类型。比如,在下面的示例程序中,a2、a2\[1\]、a2\[0\]\[2\]的类型就需要被精确地计算出来,这样才能在编译器后端生成正确的汇编代码。
|
||
|
||
```plain
|
||
let a2:number[][] = [[1,2,3],[4,5]];
|
||
println(typeof a2); //类型是number[][]
|
||
println(typeof a2[1]); //类型是number[]
|
||
println(typeof a2[0][2]); //类型是number
|
||
|
||
```
|
||
|
||
你可以在语义分析后的AST中看到这些表达式的类型信息,我在图片中都标注出来了。
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/24/f1/241810b96e75c4b4f6aed3950d9ec7f1.png?wh=996x1252)
|
||
|
||
你也可以看看typeof运算所打印出的结果。不过,TypeScript的typeof运算符输出的类型信息比较有限,只能看出a2和a2\[1\]是object类型,而a2\[0\]\[2\]是number类型,缺少更多的细节。
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/9d/00/9df8a569b7a6a8111f217a0450809f00.png?wh=488x190)
|
||
|
||
最后,我们还要让ArrayType能够像NamedType、ValueType等类型一样,采用集合运算的方法进行类型计算,以支持联合类型、类型窄化等场景。这里我就先不展开了,你可以看看[TypeUtil](https://gitee.com/richard-gong/craft-a-language/blob/master/28/types.ts)中的实现。
|
||
|
||
好了,完成与类型有关的处理以后,我们还要增强一下另一个功能,就是**左值分析**。
|
||
|
||
你还记得什么是左值吗?在表达式中,能够出现在赋值运算符左边的,就叫做左值。左值相当于是一个变量的地址。你通过这个地址,可以修改变量的值。
|
||
|
||
在下面的示例程序中,我们可以给数组元素赋值,那么这些数组元素也必须是左值才可以:
|
||
|
||
```plain
|
||
let a2:number[][] = [[1,2,3],[4,5]];
|
||
a2[0] = [4,5,6];
|
||
a2[0][1] = 7;
|
||
|
||
```
|
||
|
||
具体的实现,可以看一下[LeftValueAttributor](https://gitee.com/richard-gong/craft-a-language/blob/master/28/semantic.ts#L437)中的相关代码。
|
||
|
||
好了,完成上面工作以后,我们对编译器前端的增强就差不多了。接下来我们看看运行时的功能。
|
||
|
||
## 增强AST解释器
|
||
|
||
我们首先看看AST解释器。我们之前版本的AST解释器并不支持数组操作,所以我们把这个功能补上,以便跟编译成可执行程序的版本互相对比和验证。
|
||
|
||
要增强AST解释器,其实要做的工作还真不是太多。主要就是增加了**对下标表达式(IndexedExp)的处理**。它能返回下标表达式的值,包括左值和右值,分别用于对数组元素的写和读。
|
||
|
||
```plain
|
||
visitIndexedExp(exp:IndexedExp):any{
|
||
//下标应该是个整数
|
||
let index = this.visit(exp.indexExp);
|
||
|
||
let v = this.visit(exp.baseExp);
|
||
if (exp.isLeftValue){ //返回左值
|
||
if (v instanceof VarSymbol){
|
||
return new ArrayElementAddress(v, [index]);
|
||
}
|
||
else if (v instanceof ArrayElementAddress){
|
||
let indices = v.indices.concat([index]);
|
||
return new ArrayElementAddress(v.varSym, indices);
|
||
}
|
||
else{
|
||
console.log("Runtime error, '" + exp.baseExp.toString() + "' should return a left value");
|
||
}
|
||
}
|
||
else{ //返回右值
|
||
if (v instanceof VarSymbol){
|
||
let values = this.getVariableValue(v) as [];
|
||
return values[index];
|
||
}
|
||
else{ //如果是多维数组,比如a[0][1],那么访问baseExp的时候(a[0]),会返回一个一维数组。
|
||
let values = v as[];
|
||
return values[index];
|
||
}
|
||
}
|
||
}
|
||
|
||
```
|
||
|
||
这样修改完毕AST解释器以后,你可以用一个小小的测试程序看看它能否正常运行。比如你可以运行一下面的测试程序:
|
||
|
||
```plain
|
||
//字符串数组
|
||
let names:string[] = ["richard","sam", "john"];
|
||
names[1] = "julia"; //数组元素赋值,左值
|
||
for (let i = 0; i< 3; i++){
|
||
println(names[i]); //读取数组元素,右值
|
||
}
|
||
|
||
//number数组
|
||
let ages:number[] = [8, 18, 28];
|
||
ages[2] = 38; //数组元素赋值,左值
|
||
let sum:number = 0;
|
||
for (let i = 0; i<3; i++){
|
||
sum = sum + ages[i];//读取数组元素,右值
|
||
}
|
||
println(sum);
|
||
|
||
//二维数组
|
||
let a:number[][] = [[1,2,3],[4,5]]; //二维数组
|
||
a[0] = [4,5,6]; //修改一个维度
|
||
a[0][1] = 7; //修改一个元素
|
||
println(a); //打印二维数组
|
||
println(a[1]); //打印一维数组
|
||
println(a[0][2]); //打印一个元素
|
||
|
||
```
|
||
|
||
这个程序的输出结果我放在下面了,你能看到它确实能够处理数组了。
|
||
|
||
![图片](https://static001.geekbang.org/resource/image/e4/08/e48e8d0339386a88674b1e9a787f1e08.png?wh=488x338)
|
||
|
||
那接下来,我们再接再厉,让PlayScript能够把带有数组处理功能的程序编译成可执行文件。根据我们上两节课的经验,我们需要先设计一下与数组有关的内部对象以及相应的内置函数。
|
||
|
||
## 运行时的功能:PlayArray对象和内置函数
|
||
|
||
我们把内部的数组对象叫做PlayArray。PlayArray的设计与上一节课的PlayString有点类似,你可以看看它的代码:
|
||
|
||
```plain
|
||
typedef struct _PlayArray{
|
||
Object object;
|
||
//字符串的长度
|
||
size_t length;
|
||
|
||
//后面跟着的是数组数据
|
||
//我们不需要保存这个指针,只需要在PlayArray对象地址的基础上增加一个偏移量就行。
|
||
// void * data;
|
||
}PlayArray;
|
||
|
||
```
|
||
|
||
从代码中你能看出,PlayArray对象有一个对象头、还有一个字段记录数组中的元素个数,再后面跟着的就是具体的数组数据。
|
||
|
||
并且,我们也提供了几个内置函数,来支持基本的数组操作。其中最重要的就是创建指定长度的数组。我还写了一个宏来计算数组中某个元素的地址,这样我们就能对数组进行读写操作。
|
||
|
||
```plain
|
||
//创建指定元素个数的数组
|
||
PlayArray* array_create_by_length(size_t length);
|
||
|
||
//获取数组中某个元素的地址
|
||
#define PTR_ARRAY_ELEM(parr, index) (void *)(parr + sizeof(Object) + sizeof(size_t) + sizeof(double)*index)
|
||
|
||
```
|
||
|
||
在PlayArray对象的设计中,有一个问题需要你注意,就是怎么存储不同类型的数组元素。比如,PlayScript现在已经支持了浮点型数据和字符串数据,那如何用同一个PlayScript对象来存储这两种数据呢?
|
||
|
||
其实,对PlayArray对象来说,它并不关心存储的到底是什么类型的元素,它只关心这些元素占据多少个字节的内存,便于申请内存。
|
||
|
||
对于double数据来说,它们占据8个字节64位。而对于PlayString对象来说,我们只需要存储对象的引用,也就是对象的内存地址,这个地址也是8个字节64位的。所以,现在这个版本的PlayArray中,我们暂且认为每个元素占据8个字节就好了。如果数组大小是n,那就要为数组数据申请n\*8个字节的内存。
|
||
|
||
你也可以看到,在上面的宏中,如果你要获取数组元素的地址,会返回一个void \*。你可以把它强制转换成其他类型,比如转换成double \*,就能存取double型的数据。转换成PlayScript \*_,就能存取字符串对象的引用,也就是PlayScript_。
|
||
|
||
我用C语言做了一个示例程序[array.c](https://gitee.com/richard-gong/craft-a-language/blob/master/28/array.c),来测试PlayArray对象有关的功能。你可以看一下我是如何获取数组元素的地址,并给数组元素赋值的。比如下面这个程序就是给数组元素赋double值:
|
||
|
||
```plain
|
||
//创建示例的number数组
|
||
PlayArray* sample_array_double(){
|
||
//创建数组
|
||
PlayArray * parr = array_create_by_length(3);
|
||
|
||
//给数据元素赋值
|
||
*((double *)PTR_ARRAY_ELEM(parr,0)) = 5;
|
||
*((double *)PTR_ARRAY_ELEM(parr,1)) = 10.5;
|
||
*((double *)PTR_ARRAY_ELEM(parr,2)) = 10.6;
|
||
|
||
return parr;
|
||
}
|
||
|
||
```
|
||
|
||
然后你可以再把这些数据从double数组里读出来,并进行汇总。
|
||
|
||
```plain
|
||
//把number数组的元素汇总
|
||
double sum_array_double(PlayArray * parr){
|
||
//读取数据并汇总
|
||
double sum = 0;
|
||
for (int i = 0; i< parr->length; i++){
|
||
sum += *((double *)PTR_ARRAY_ELEM(parr,i));
|
||
}
|
||
|
||
return sum;
|
||
}
|
||
|
||
```
|
||
|
||
对于字符串数组,我们也可以进行类似的处理。只不过,现在存取的对象是PlayScript\*,也就是字符串引用。
|
||
|
||
```plain
|
||
//创建示例的字符串数组
|
||
PlayArray* sample_array_string(){
|
||
//创建数组
|
||
PlayArray * parr = array_create_by_length(2);
|
||
|
||
//给数据元素赋值
|
||
*((PlayString **)PTR_ARRAY_ELEM(parr,0)) = string_create_by_cstr("Hello");
|
||
*((PlayString **)PTR_ARRAY_ELEM(parr,1)) = string_create_by_cstr(" PlayScript!");
|
||
|
||
return parr;
|
||
}
|
||
|
||
//把字符串数组的元素拼接在一起,形成一个新的字符串
|
||
PlayString* concat_array_string(PlayArray * parr){
|
||
PlayString* pstr;
|
||
|
||
if (parr->length > 0) pstr = *((PlayString**)PTR_ARRAY_ELEM(parr, 0));
|
||
|
||
for (int i = 1; i< parr->length; i++){
|
||
PlayString* pstr1 = *((PlayString**)PTR_ARRAY_ELEM(parr, i));
|
||
pstr = string_concat(pstr, pstr1);
|
||
}
|
||
|
||
return pstr;
|
||
}
|
||
|
||
```
|
||
|
||
甚至,你还可以往数组里存其他数组的对象引用,这也就是多维数组的情况:
|
||
|
||
```plain
|
||
//创建一个示例的二维数组,其中一个维度是double类型,另一个维度是字符串数组
|
||
PlayArray * sample_array_2d(){
|
||
//创建数组
|
||
PlayArray * parr = array_create_by_length(2);
|
||
|
||
*((PlayArray **)PTR_ARRAY_ELEM(parr,0)) = sample_array_double();
|
||
*((PlayArray **)PTR_ARRAY_ELEM(parr,1)) = sample_array_string();
|
||
|
||
return parr;
|
||
}
|
||
|
||
```
|
||
|
||
最后,你可以用make array命令,编译一下array.c示例程序,然后用./array来运行一下这个示例程序,看看它是否成功完成了与数组有关的操作,包括创建数组、往数组里存不同类型的数据,以及从数组里读取数据。
|
||
|
||
好,现在我们已经完成了运行时的准备工作了。现在只差最后一步工作了,就是修改编译器后端,来生成正确的汇编代码,从而编译成可执行文件。
|
||
|
||
## 修改编译器后端
|
||
|
||
在编译器的后端,为了支持数组运算,最重要的是完成下面几项工作:**根据数组字面量来创建数组对象,获取数组元素的地址,以及对寄存器分配算法进行必要的调整**。
|
||
|
||
我们先来看看第一项任务:把数组字面量转变成数组对象。
|
||
|
||
这是在[visitArrayLiteral](https://gitee.com/richard-gong/craft-a-language/blob/master/28/asm_x86-64.ts#L1436)中处理的。首先,它要调用内置函数创建一个数组对象。接着,要再计算出数组元素的地址,以便给数组的元素赋值。你可以查看一下示例代码。
|
||
|
||
第二项任务,是获取数组元素的地址。
|
||
|
||
在array.c示例程序中,你已经看到,在读写数组元素的时候,我们必须获取数组元素的地址。在生成汇编代码的时候,我们也要完成类似的任务。
|
||
|
||
首先,我们要根据PlayArray对象的地址计算出数组数据的地址。你已经知道,所有对象头的大小是16个字节,而记录数组元素个数的length字段占据4个字节。所以,你可以用一条指令把PlayArray的地址加上20,得到数组数据的地址。
|
||
|
||
那怎么访问数组中的某个元素呢?我们在汇编代码中已经使用过内存访问的操作数,就是基于寄存器的值,再加上或者减去一个偏移量的形式。假设我们把数组数据的开头地址存放到rax寄存器里,那么数组的第一个元素的内存地址是(%rax),第二个元素的地址是8(%rax),第三个元素的地址是16(%rax),依次类推。
|
||
|
||
不过,上面这种格式是间接内存访问的简化形式。如果你还记得间接内存地址访问的完整形式,那么上述两个步骤可以合并成一条指令,比如使用20(%rax,%rdi,8)这样的格式可以直接计算出数组元素的地址。其中,20是对象头的偏移量,%rdi的值假设是2,2指的是第二个数组元素,8指的是每个数组元素占据8个字节。所以最后的地址是:%rax + 2\*8 + 20,正好就是第2个数组元素的地址。
|
||
|
||
第三项任务,是对寄存器分配算法进行必要的调整。
|
||
|
||
在对数组元素进行操作的时候,我们最好也先把它们挪到寄存器,在寄存器里进行相关计算。相关示例代码你可以查看[Lower类](https://gitee.com/richard-gong/craft-a-language/blob/master/28/asm_x86-64.ts#L1636)。
|
||
|
||
在对编译器的后端进行了上述修改以后,你就可以用它来编译带有数组运算的代码,并且生成可执行程序了。你可以参考[example\_array.ts](https://gitee.com/richard-gong/craft-a-language/blob/master/28/example_array.ts)示例代码,用make example\_array命令编译成汇编代码和可执行程序。你也可以多写点不同的程序玩一玩。
|
||
|
||
看着现在我们的语言能够支持复杂的、与数组有关的语法,甚至能支持多维数组,你会不会觉得很有成就感呢?
|
||
|
||
## 课程小结
|
||
|
||
这就是今天的所有的内容了。今天我们用了一节课的时间,给PlayScript增加了处理数组的能力。我建议你记住几个关键点:
|
||
|
||
首先,是语法处理方面的知识点。在今天的语法规则中,有两处规则如果处理不好,就会变成左递归文法。比如声明多维数组类型let a : number\[\]\[\]时,以及访问多维数组元素时,如a\[i\]\[j\]。你要看看我是怎么处理这两处文法的。在之前的课程中,我们处理连续赋值表达式,比如a=b=c的时候,我也使用了类似的文法和解析程序。如果后面有机会,我会把这个知识点再展开给你讲一讲。
|
||
|
||
第二,是内存布局设计方面的知识点。我们用一个统一的PlayScript对象来保存各种数据,包括基础数据number,以及以对象格式保存的String,甚至其他数组对象的引用。下一节课我们会讲到自定义对象,到时候你仍然可以把这些对象的引用保存到数组里。
|
||
|
||
第三,是关于如何访问数组的元素。在语义分析阶段,我们要能够区分左值和右值的场景。左值返回的是地址,而右值返回的是地址中的值。而在编译器的后端,你需要知道如何正确的计算数组元素的地址,从而转化成正确的操作数。
|
||
|
||
## 思考题
|
||
|
||
目前的设计中,数组中存放的元素的大小都是8个字节。如果我们需要存储其他长度的元素,比如4个字节的整型,应该如何修改当前的设计?哪些地方的程序也需要进行调整?欢迎在留言区分享你的观点。
|
||
|
||
欢迎把这节课分享给更多感兴趣的朋友。我是宫文学,我们下节课见。
|
||
|
||
## 资源链接
|
||
|
||
[这节课的示例代码在这里!](https://gitee.com/richard-gong/craft-a-language/tree/master/28)
|
||
|