Post

Support single-letter local variables

支持单字母本地变量。需要注意的是他这里只支持小写字母。

知识点

这次commit主要的修改还是parse和codegen,tokenize有小的修改

一.tokenize部分修改

tokenize部分在TokenKind增加了一个TK_IDENT(标识符)类型,现在切割的标识符就是 a-z 的字母

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
Token *tokenize(void) {
  //..
  while (*p) {
    // Skip whitespace characters.
    //...
    // Keywords
    //...
    // Identifier
    if ('a' <= *p && *p <= 'z') {
      cur = new_token(TK_IDENT, cur, p++, 1);
      continue;
    }
    //..
  }
}

相应的增加一个消耗标识符的函数

1
2
3
4
5
6
7
Token *consume_ident(void) {
  if (token->kind != TK_IDENT)
    return NULL;
  Token *t = token;
  token = token->next;
  return t;
}

二.parse部分修改

既然是本地变量那么就涉及到变量的赋值(现在还没有变量定义的语法),作者给的语法定义是这样的

1
2
3
// expr = assign
// assign = equality ("=" assign)?
// primary = "(" expr ")" | ident | num 

原来的 expr=equality 改成了 expr = assign

赋值到现在来说优先级是最低的,所以expr要先parse assign表达式。

identnum一样归属到primary是典型写法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
static Node *assign(void) {
  Node *node = equality();
  if (consume("="))
    node = new_binary(ND_ASSIGN, node, assign());
  return node;
}

static Node *primary(void) {
  if (consume("(")) {
    Node *node = expr();
    expect(")");
    return node;
  }

  Token *tok = consume_ident();
  if (tok)
    return new_var_node(*tok->str);

  return new_num(expect_number());
}

三.codegen部分修改

codegen部分涉及四个方面:栈空间的分配、怎样根据变量寻址、怎样对变量赋值、怎样获取到变量值

1.栈空间的分配

由于变量名只能是从 a-z 的单字母,不管3721他直接在栈上分配208个空间,208正好是26个字母变量每个占用8个字节

1
  printf("  sub rsp, 208\n");

RISC-V 版

1
  printf("  addi sp, sp, -208\n");

2.根据变量寻址

既然26个字母的空间都分配好了,那么 a-z 变量就跟数组一样从 0-25 排列访问就可以了

1
2
3
4
5
6
7
8
static void gen_addr(Node *node) {
    //...
    int offset = (node->name - 'a' + 1) * 8;
    printf("  lea rax, [rbp-%d]\n", offset);
    printf("  push rax\n");
    return;
    //...
}

RISC-V 版

1
2
3
4
5
6
7
static void genAddr(Node *Nd) {
  //...
  int offset = (node->name - 'a' + 1) * 8;
  printf("  addi a0, fp, %d\n", -Offset);
  return;
  //...
}

3.变量赋值

搞定了前面的变量寻址,变量赋值就是将等号右边表达式的值拷贝到变量的地址空间里

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
static void store(void) {
  printf("  pop rdi\n");        //弹出右边=表达式值
  printf("  pop rax\n");        //弹出变量地址
  printf("  mov [rax], rdi\n"); //赋值到变量地址里
  printf("  push rdi\n");  
}

static void gen(Node *node) {
  switch (node->kind) {
    //...
    case ND_ASSIGN:
      gen_addr(node->lhs);      
      gen(node->rhs);
      store();
      return;
  }
  //...
}

RISC-V 版

1
2
3
4
5
6
7
8
9
10
11
12
13
static void gen(Node *node) {
  switch (node->kind) {
    //...
    case ND_ASSIGN:
      gen_addr(node->lhs);
      push();//将地址压入栈中
      gen(node->rhs);
      pop("a1");//弹出地址到a1
      printf("  sd a0, 0(a1)\n");//将右值存储到地址中
      return;
  }
  //...
}

4.获取变量值

由于少了右边的表达式,获取变量值比变量赋值更简单了,只要拿到变量的地址取出地址里的数据即可

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
static void load(void) {
  printf("  pop rax\n");        ///弹出变量地址
  printf("  mov rax, [rax]\n"); //取出地址里的数据
  printf("  push rax\n");       
}
static void gen(Node *node) {
  switch (node->kind) {
    //...
    case ND_VAR:
      gen_addr(node);
      load();
      return;
    //..
  }
}

RISC-V 版

1
2
3
4
5
6
7
8
9
10
static void gen(Node *node) {
  switch (node->kind) {
    //...
    case ND_VAR:
      gen_addr(node);
      printf("  ld a0, 0(a0)\n");//a0地址中存储的数据加载到a0
      return;
    //..
  }
}

5.其他

这次他生成的汇编是一个典型的函数模版(我在Add the notion of the expression statement中提到)。由于之前ND_RETURN节点直接用ret指令跳出,这次由于分配了栈空间给26个字母变量所以不能直接这么退出了,他改成了跳转label的方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
static void gen(Node *node) {
  //...
  switch (node->kind) {
  //...
  case ND_RETURN:
      gen(node->lhs);
      printf("  pop rax\n");
      printf("  jmp .L.return\n");
      return;
   //...
  }
}
void codegen(Node *node) {
  //...
  printf("  push rbp\n");
  printf("  mov rbp, rsp\n");
  printf("  sub rsp, 208\n");

  //...
  printf(".L.return:\n");
  printf("  mov rsp, rbp\n");
  printf("  pop rbp\n");
  printf("  ret\n");
}

RISC-V 版

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
static void gen(Node *node) {
  //...
  switch (node->kind) {
  //...
  case ND_RETURN:
      gen(node->lhs);
      //为什么RISC-V 不像x86 pop呢?因为计算不靠栈而是用寄存器
      printf("  j .L.return\n");    //RISC-V直接用j比x86的jmp更省字母
      return;
   //...
  }
}
void codegen(Node *node) {
  //...
  // 将fp压到栈上。fp=Frame Pointer,作用相当于x86的rbp为了退出时恢复sp
  printf("  addi sp, sp, -8\n");
  printf("  sd fp, 0(sp)\n");
  // fp=sp,思路跟 mov rbp,rsp一样
  printf("  mv fp, sp\n");
  //分配栈空间
  printf("  addi sp, sp, -208\n");


  //...


  printf(".L.return:\n");
  // 将fp的值改写回sp(丢弃掉分配的栈空间),等效于mov rsp, rbp
  printf("  mv sp, fp\n");
  // 弹出fp,恢复fp。等效于pop rbp
  printf("  ld fp, 0(sp)\n");
  printf("  addi sp, sp, 8\n");
  printf("  ret\n");
}
This post is licensed under CC BY 4.0 by the author.