第 8 部分:If 语句

现在我们可以比较值了,是时候将 IF 语句添加到我们的语言中了。因此,首先让我们看一下 IF 语句的一般语法以及它们如何转换为汇编语言。

IF 语法

IF 语句的语法为:

  if (condition is true) 
    perform this first block of code
  else
    perform this other block of code

现在,通常如何将其转换为汇编语言?事实证明,如果相反的比较为真,我们会进行相反的比较并跳转/分支:

       perform the opposite comparison
       jump to L1 if true
       perform the first block of code
       jump to L2
L1:
       perform the other block of code
L2:
   

其中L1L2是汇编语言标签。

在我们的编译器中生成程序集

现在,我们输出代码来根据比较设置寄存器,例如

   int x; x= 7 < 9;         From input04

变成

        movq    $7, %r8
        movq    $9, %r9
        cmpq    %r9, %r8
        setl    %r9b        Set if less than 
        andq    $255,%r9

但对于 IF 语句,我们需要进行相反的比较:

   if (7 < 9) 

应该变成:

        movq    $7, %r8
        movq    $9, %r9
        cmpq    %r9, %r8
        jge     L1         Jump if greater then or equal to
        ....
L1:

因此,我在这部分旅程中实现了 IF 语句。由于这是一个工作项目,我确实必须撤消一些事情并重构它们作为旅程的一部分。我将尽力介绍沿途的更改和添加。

新代币和悬空的其他代币

我们的语言将需要一堆新的标记。我(现在)也想避免悬空的 else 问题。为此,我更改了语法,以便所有语句组都用“{”…“}”大括号括起来;我将这样的分组称为“复合语句”。我们还需要“(”…“)”括号来保存 IF 表达式,以及关键字“if”和“else”。因此,新的代币是(在 中defs.h):

  T_LBRACE, T_RBRACE, T_LPAREN, T_RPAREN,
  // Keywords
  ..., T_IF, T_ELSE

扫描令牌

单字符标记应该是显而易见的,我不会给出扫描它们的代码。关键字也应该非常明显,但我将给出来自 keyword()以下的扫描代码scan.c

  switch (*s) {
    case 'e':
      if (!strcmp(s, "else"))
        return (T_ELSE);
      break;
    case 'i':
      if (!strcmp(s, "if"))
        return (T_IF);
      if (!strcmp(s, "int"))
        return (T_INT);
      break;
    case 'p':
      if (!strcmp(s, "print"))
        return (T_PRINT);
      break;
  }

新 BNF 语法

我们的语法开始变得庞大,所以我稍微重写了它:

 compound_statement: '{' '}'          // empty, i.e. no statement
      |      '{' statement '}'
      |      '{' statement statements '}'
      ;

 statement: print_statement
      |     declaration
      |     assignment_statement
      |     if_statement
      ;

 print_statement: 'print' expression ';'  ;

 declaration: 'int' identifier ';'  ;

 assignment_statement: identifier '=' expression ';'   ;

 if_statement: if_head
      |        if_head 'else' compound_statement
      ;

 if_head: 'if' '(' true_false_expression ')' compound_statement  ;

 identifier: T_IDENT ;

我省略了 的定义true_false_expression,但在某个时候,当我们添加更多运算符时,我会将其添加进去。

请注意 IF 语句的语法:它要么是一个if_head(没有“else”子句),要么是一个if_head后跟“else”和一个compound_statement

我已经将所有不同的语句类型分开,以拥有自己的非终端名称。另外,之前的statements非终结符现在compound_statement 是非终结符,这需要在语句周围使用 ’{’ … ’}’。

这意味着compound_statement头部的 被 ’{’ … ’}’ 包围,compound_statement’else’ 关键字之后的任何内容也是如此。因此,如果我们有嵌套的 IF 语句,它们必须如下所示:

  if (condition1 is true) {
    if (condition2 is true) {
      statements;
    } else {
      statements;
    }
  } else {
    statements;
  }

并且每个“其他”属于哪个“如果”是没有歧义的。这解决了悬空的 else 问题。稍后,我会将 ’{’ … ’}’ 设为可选。

解析复合语句

void statements()函数现在compound_statement()如下所示:

// Parse a compound statement
// and return its AST
struct ASTnode *compound_statement(void) {
  struct ASTnode *left = NULL;
  struct ASTnode *tree;
 
  // Require a left curly bracket
  lbrace();
 
  while (1) {
    switch (Token.token) {
      case T_PRINT:
        tree = print_statement();
        break;
      case T_INT:
        var_declaration();
        tree = NULL;            // No AST generated here
        break;
      case T_IDENT:
        tree = assignment_statement();
        break;
      case T_IF:
        tree = if_statement();
        break;
    case T_RBRACE:
        // When we hit a right curly bracket,
        // skip past it and return the AST
        rbrace();
        return (left);
      default:
        fatald("Syntax error, token", Token.token);
    }
 
    // For each new tree, either save it in left
    // if left is empty, or glue the left and the
    // new tree together
    if (tree) {
      if (left == NULL)
        left = tree;
      else
        left = mkastnode(A_GLUE, left, NULL, tree, 0);
    }
  }

首先,请注意,代码强制解析器将复合语句开头的“{”与 相匹配lbrace(),并且只有当我们将结尾的“}”与 相匹配时才能退出rbrace()

其次,请注意print_statement()assignment_statement()if_statement()返回一个 AST 树,就像 一样compound_statement()。在我们的旧代码中,print_statement()调用自身genAST()来计算表达式,然后调用genprintint(). 同样, assignment_statement() 也叫做genAST()作业。

嗯,这意味着我们这里有 AST 树,那里有其他树。genAST()仅生成一棵 AST 树并调用一次为其生成汇编代码是有意义的。

这不是强制性的。例如,SubC 仅为表达式生成 AST。对于语言的结构部分(例如语句),SubC 对代码生成器进行特定调用,就像我在以前版本的编译器中所做的那样。

我现在决定使用解析器为整个输入生成一个 AST 树。一旦输入被解析,就可以从一棵 AST 树生成汇编输出。

稍后,我可能会为每个函数生成一个 AST 树。之后。

解析 IF 语法

因为我们是递归下降解析器,所以解析 IF 语句还不错:

// Parse an IF statement including
// any optional ELSE clause
// and return its AST
struct ASTnode *if_statement(void) {
  struct ASTnode *condAST, *trueAST, *falseAST = NULL;
 
  // Ensure we have 'if' '('
  match(T_IF, "if");
  lparen();
 
  // Parse the following expression
  // and the ')' following. Ensure
  // the tree's operation is a comparison.
  condAST = binexpr(0);
 
  if (condAST->op < A_EQ || condAST->op > A_GE)
    fatal("Bad comparison operator");
  rparen();
 
  // Get the AST for the compound statement
  trueAST = compound_statement();
 
  // If we have an 'else', skip it
  // and get the AST for the compound statement
  if (Token.token == T_ELSE) {
    scan(&Token);
    falseAST = compound_statement();
  }
  // Build and return the AST for this statement
  return (mkastnode(A_IF, condAST, trueAST, falseAST, 0));
}

现在,我不想处理像 那样的输入if (x-2),所以我限制了二进制表达式 from 的binexpr()根,它是六个比较运算符 A_EQ、A_NE、A_LT、A_GT、A_LE 或 A_GE 之一。

第三个孩子

我差点儿在没有正确解释的情况下就从你身边走私了一些东西。在最后一行,if_statement()我使用以下命令构建了 AST 节点:

   mkastnode(A_IF, condAST, trueAST, falseAST, 0);

那是三个AST 子树!这里发生了什么?正如您所看到的,IF 语句将有三个子语句:

  • 评估条件的子树
  • 紧随其后的复合语句
  • “else”关键字后面的可选复合语句

所以我们现在需要一个具有三个子节点的 AST 节点结构(在 中defs.h):

// AST node types.
enum {
  ...
  A_GLUE, A_IF
};
 
// Abstract Syntax Tree structure
struct ASTnode {
  int op;                       // "Operation" to be performed on this tree
  struct ASTnode *left;         // Left, middle and right child trees
  struct ASTnode *mid;
  struct ASTnode *right;
  union {
    int intvalue;               // For A_INTLIT, the integer value
    int id;                     // For A_IDENT, the symbol slot number
  } v;
};

因此,A_IF 树如下所示:

                      IF
                    / |  \
                   /  |   \
                  /   |    \
                 /    |     \
                /     |      \
               /      |       \
      condition   statements   statements

粘合 AST 节点

还有一个新的 A_GLUE AST 节点类型。这是做什么用的?我们现在构建了一个包含大量语句的 AST 树,因此我们需要一种方法将它们粘合在一起。

查看循环结束代码compound_statement()

      if (left != NULL)
        left = mkastnode(A_GLUE, left, NULL, tree, 0);

每次我们获得一个新的子树时,我们都会将其粘合到现有的树上。因此,对于这一系列语句:

    stmt1;
    stmt2;
    stmt3;
    stmt4;

我们最终得到:

             A_GLUE
              /  \
          A_GLUE stmt4
            /  \
        A_GLUE stmt3
          /  \
      stmt1  stmt2

而且,当我们从左到右深度优先遍历树时,这仍然会按正确的顺序生成汇编代码。

通用代码生成器

现在我们的 AST 节点有多个子节点,我们的通用代码生成器将变得更加复杂。另外,对于比较运算符,我们需要知道我们是否将比较作为 IF 语句的一部分(跳转到相反的比较)或普通表达式(在普通比较中将寄存器设置为 1 或 0)。

为此,我进行了修改,getAST()以便我们可以传入父 AST 节点操作:

// Given an AST, the register (if any) that holds
// the previous rvalue, and the AST op of the parent,
// generate assembly code recursively.
// Return the register id with the tree's final value
int genAST(struct ASTnode *n, int reg, int parentASTop) {
   ...
}

处理特定 AST 节点

现在的代码genAST()必须处理特定的 AST 节点:

  // We now have specific AST node handling at the top
  switch (n->op) {
    case A_IF:
      return (genIFAST(n));
    case A_GLUE:
      // Do each child statement, and free the
      // registers after each child
      genAST(n->left, NOREG, n->op);
      genfreeregs();
      genAST(n->right, NOREG, n->op);
      genfreeregs();
      return (NOREG);
  }

如果我们不返回,我们将继续执行正常的二元运算符 AST 节点,但有一个例外:比较节点:

    case A_EQ:
    case A_NE:
    case A_LT:
    case A_GT:
    case A_LE:
    case A_GE:
      // If the parent AST node is an A_IF, generate a compare
      // followed by a jump. Otherwise, compare registers and
      // set one to 1 or 0 based on the comparison.
      if (parentASTop == A_IF)
        return (cgcompare_and_jump(n->op, leftreg, rightreg, reg));
      else
        return (cgcompare_and_set(n->op, leftreg, rightreg));

我将介绍新功能cgcompare_and_jump()cgcompare_and_set()以下内容。

生成 IF 汇编代码

我们使用特定函数处理 A_IF AST 节点,以及生成新标签编号的函数:

// Generate and return a new label number
static int label(void) {
  static int id = 1;
  return (id++);
}
 
// Generate the code for an IF statement
// and an optional ELSE clause
static int genIFAST(struct ASTnode *n) {
  int Lfalse, Lend;
 
  // Generate two labels: one for the
  // false compound statement, and one
  // for the end of the overall IF statement.
  // When there is no ELSE clause, Lfalse _is_
  // the ending label!
  Lfalse = label();
  if (n->right)
    Lend = label();
 
  // Generate the condition code followed
  // by a zero jump to the false label.
  // We cheat by sending the Lfalse label as a register.
  genAST(n->left, Lfalse, n->op);
  genfreeregs();
 
  // Generate the true compound statement
  genAST(n->mid, NOREG, n->op);
  genfreeregs();
 
  // If there is an optional ELSE clause,
  // generate the jump to skip to the end
  if (n->right)
    cgjump(Lend);
 
  // Now the false label
  cglabel(Lfalse);
 
  // Optional ELSE clause: generate the
  // false compound statement and the
  // end label
  if (n->right) {
    genAST(n->right, NOREG, n->op);
    genfreeregs();
    cglabel(Lend);
  }
 
  return (NOREG);
}

实际上,代码正在执行以下操作:

  genAST(n->left, Lfalse, n->op);       // Condition and jump to Lfalse
  genAST(n->mid, NOREG, n->op);         // Statements after 'if'
  cgjump(Lend);                         // Jump to Lend
  cglabel(Lfalse);                      // Lfalse: label
  genAST(n->right, NOREG, n->op);       // Statements after 'else'
  cglabel(Lend);                        // Lend: label

x86-64 代码生成函数

现在我们有了一些新的 x86-64 代码生成函数。其中一些取代了cgXXX()我们在旅程的最后部分创建的六个比较函数。

对于普通的比较函数,我们现在传入 AST 操作来选择相关的 x86-64set指令:

// List of comparison instructions,
// in AST order: A_EQ, A_NE, A_LT, A_GT, A_LE, A_GE
static char *cmplist[] =
  { "sete", "setne", "setl", "setg", "setle", "setge" };
 
// Compare two registers and set if true.
int cgcompare_and_set(int ASTop, int r1, int r2) {
 
  // Check the range of the AST operation
  if (ASTop < A_EQ || ASTop > A_GE)
    fatal("Bad ASTop in cgcompare_and_set()");
 
  fprintf(Outfile, "\tcmpq\t%s, %s\n", reglist[r2], reglist[r1]);
  fprintf(Outfile, "\t%s\t%s\n", cmplist[ASTop - A_EQ], breglist[r2]);
  fprintf(Outfile, "\tmovzbq\t%s, %s\n", breglist[r2], reglist[r2]);
  free_register(r1);
  return (r2);
}

我还发现了一条 x86-64 指令movzbq,该指令从一个寄存器中移动最低字节并将其扩展以适合 64 位寄存器。我现在使用它而不是and $255旧代码中的。

我们需要一个函数来生成标签并跳转到它:

// Generate a label
void cglabel(int l) {
  fprintf(Outfile, "L%d:\n", l);
}
 
// Generate a jump to a label
void cgjump(int l) {
  fprintf(Outfile, "\tjmp\tL%d\n", l);
}

最后,我们需要一个函数来进行比较并根据相反的比较进行跳转。因此,使用 AST 比较节点类型,我们进行相反的比较:

// List of inverted jump instructions,
// in AST order: A_EQ, A_NE, A_LT, A_GT, A_LE, A_GE
static char *invcmplist[] = { "jne", "je", "jge", "jle", "jg", "jl" };
 
// Compare two registers and jump if false.
int cgcompare_and_jump(int ASTop, int r1, int r2, int label) {
 
  // Check the range of the AST operation
  if (ASTop < A_EQ || ASTop > A_GE)
    fatal("Bad ASTop in cgcompare_and_set()");
 
  fprintf(Outfile, "\tcmpq\t%s, %s\n", reglist[r2], reglist[r1]);
  fprintf(Outfile, "\t%s\tL%d\n", invcmplist[ASTop - A_EQ], label);
  freeall_registers();
  return (NOREG);
}

测试 IF 语句

执行make test编译input05文件的操作:

{
  int i; int j;
  i=6; j=12;
  if (i < j) {
    print i;
  } else {
    print j;
  }
}

这是最终的汇编输出:

        movq    $6, %r8
        movq    %r8, i(%rip)    # i=6;
        movq    $12, %r8
        movq    %r8, j(%rip)    # j=12;
        movq    i(%rip), %r8
        movq    j(%rip), %r9
        cmpq    %r9, %r8        # Compare %r8-%r9, i.e. i-j
        jge     L1              # Jump to L1 if i >= j
        movq    i(%rip), %r8
        movq    %r8, %rdi       # print i;
        call    printint
        jmp     L2              # Skip the else code
L1:
        movq    j(%rip), %r8
        movq    %r8, %rdi       # print j;
        call    printint
L2:

当然,还make test显示:

cc -o comp1 -g cg.c decl.c expr.c gen.c main.c misc.c
      scan.c stmt.c sym.c tree.c
./comp1 input05
cc -o out out.s
./out
6                   # As 6 is less than 12

结论和下一步是什么

我们已经使用 IF 语句将第一个控制结构添加到我们的语言中。一路上我不得不重写一些现有的东西,并且考虑到我脑子里没有完整的架构计划,我将来可能不得不重写更多的东西。

这部分旅程的问题在于,我们必须对 IF 决策执行与正常比较运算符相反的比较。我的解决方案是告知每个 AST 节点其父节点的节点类型;比较节点现在可以查看父节点是否是 A_IF 节点。

我知道 Nils Holm 在实现 SubC 时选择了不同的方法,因此您应该查看他的代码,以了解同一问题的不同解决方案。

在编译器编写之旅的下一部分中,我们将添加另一个控制结构:WHILE 循环。下一步