第五部分:声明
是时候在我们的语言语法中添加一些“正确的”语句了。我希望能够编写这样的代码行:
print 2 + 3 * 5;
print 18 - 6/3 + 4*2;
当然,由于我们忽略空格,因此一条语句的所有标记不必位于同一行。每个语句都以关键字开头print,并以分号结束。所以这些将成为我们语言中的新标记。
BNF 语法描述
我们已经了解了表达式的 BNF 表示法。现在让我们为上述类型的语句定义 BNF 语法:
statements: statement
| statement statements
;
statement: 'print' expression ';'
;
输入文件由多个语句组成。它们要么是一个语句,要么是一个语句后面跟着多个语句。每条语句都以关键字 开头print,然后是一个表达式,最后是一个分号。
词法扫描器的更改
在我们获得解析上述语法的代码之前,我们需要在现有代码中添加一些零碎的东西。让我们从词法扫描器开始。
添加分号标记将很容易。现在,print关键字。稍后,我们将在该语言中拥有许多关键字,以及变量的标识符,因此我们需要添加一些代码来帮助我们处理它们。
在 中scan.c,我添加了从 SubC 编译器借来的代码。它将字母数字字符读入缓冲区,直到遇到非字母数字字符。
// Scan an identifier from the input file and
// store it in buf[]. Return the identifier's length
static int scanident(int c, char *buf, int lim) {
int i = 0;
// Allow digits, alpha and underscores
while (isalpha(c) || isdigit(c) || '_' == c) {
// Error if we hit the identifier length limit,
// else append to buf[] and get next character
if (lim - 1 == i) {
printf("identifier too long on line %d\n", Line);
exit(1);
} else if (i < lim - 1) {
buf[i++] = c;
}
c = next();
}
// We hit a non-valid character, put it back.
// NUL-terminate the buf[] and return the length
putback(c);
buf[i] = '\0';
return (i);
}我们还需要一个函数来识别语言中的关键字。一种方法是拥有一个关键字列表,然后针对 的strcmp() 缓冲区遍历该列表和每个关键字scanident()。SubC 的代码有一个优化:在执行strcmp(). 这加快了与数十个关键字的比较。现在我们不需要这种优化,但我已将其放入稍后:
// Given a word from the input, return the matching
// keyword token number or 0 if it's not a keyword.
// Switch on the first letter so that we don't have
// to waste time strcmp()ing against all the keywords.
static int keyword(char *s) {
switch (*s) {
case 'p':
if (!strcmp(s, "print"))
return (T_PRINT);
break;
}
return (0);
}现在,在 switch 语句的底部scan(),我们添加以下代码来识别分号和关键字:
case ';':
t->token = T_SEMI;
break;
default:
// If it's a digit, scan the
// literal integer value in
if (isdigit(c)) {
t->intvalue = scanint(c);
t->token = T_INTLIT;
break;
} else if (isalpha(c) || '_' == c) {
// Read in a keyword or identifier
scanident(c, Text, TEXTLEN);
// If it's a recognised keyword, return that token
if (tokentype = keyword(Text)) {
t->token = tokentype;
break;
}
// Not a recognised keyword, so an error for now
printf("Unrecognised symbol %s on line %d\n", Text, Line);
exit(1);
}
// The character isn't part of any recognised token, error
printf("Unrecognised character %c on line %d\n", c, Line);
exit(1);我还添加了一个全局Text缓冲区来存储关键字和标识符:
#define TEXTLEN 512 // Length of symbols in input
extern_ char Text[TEXTLEN + 1]; // Last identifier scanned对表达式解析器的更改
到目前为止,我们的输入文件只包含一个表达式;binexpr()因此,在(in )中的 Pratt 解析器代码中expr.c,我们使用以下代码退出解析器:
// If no tokens left, return just the left node
tokentype = Token.token;
if (tokentype == T_EOF)
return (left);在我们的新语法中,每个表达式都以分号结尾。因此,我们需要更改表达式解析器中的代码以发现标记T_SEMI 并退出表达式解析:
// Return an AST tree whose root is a binary operator.
// Parameter ptp is the previous token's precedence.
struct ASTnode *binexpr(int ptp) {
struct ASTnode *left, *right;
int tokentype;
// Get the integer literal on the left.
// Fetch the next token at the same time.
left = primary();
// If we hit a semicolon, return just the left node
tokentype = Token.token;
if (tokentype == T_SEMI)
return (left);
while (op_precedence(tokentype) > ptp) {
...
// Update the details of the current token.
// If we hit a semicolon, return just the left node
tokentype = Token.token;
if (tokentype == T_SEMI)
return (left);
}
}代码生成器的更改
我想将通用代码生成器gen.c 与cg.c. 这也意味着编译器的其余部分应该只调用 中的函数 gen.c,并且只gen.c应该调用 中的代码cg.c。
为此,我在中定义了一些新的“前端”函数gen.c:
void genpreamble() { cgpreamble(); }
void genpostamble() { cgpostamble(); }
void genfreeregs() { freeall_registers(); }
void genprintint(int reg) { cgprintint(reg); }添加语句解析器
我们有一个新文件stmt.c。这将保存我们语言中所有主要语句的解析代码。现在,我们需要解析我上面放弃的语句的 BNF 语法。这是通过这个单一函数完成的。我已将递归定义转换为循环:
// Parse one or more statements
void statements(void) {
struct ASTnode *tree;
int reg;
while (1) {
// Match a 'print' as the first token
match(T_PRINT, "print");
// Parse the following expression and
// generate the assembly code
tree = binexpr(0);
reg = genAST(tree);
genprintint(reg);
genfreeregs();
// Match the following semicolon
// and stop if we are at EOF
semi();
if (Token.token == T_EOF)
return;
}
}在每个循环中,代码都会找到一个 T_PRINT 标记。然后它调用binexpr()解析表达式。最后,它找到了 T_SEMI 令牌。如果后面有 T_EOF 标记,我们就会跳出循环。
在每个表达式树之后,调用 in 中的代码gen.c将树转换为汇编代码并调用汇编printint()函数打印出最终值。
一些辅助函数
上面的代码中有几个新的辅助函数,我已将其放入一个新文件中misc.c:
// Ensure that the current token is t,
// and fetch the next token. Otherwise
// throw an error
void match(int t, char *what) {
if (Token.token == t) {
scan(&Token);
} else {
printf("%s expected on line %d\n", what, Line);
exit(1);
}
}
// Match a semicon and fetch the next token
void semi(void) {
match(T_SEMI, ";");
}这些构成解析器中语法检查的一部分。稍后,我将添加更多短函数来调用,match()以使我们的语法检查更容易。
更改为main()
main()用于binexpr()直接调用来解析旧输入文件中的单个表达式。现在它这样做:
scan(&Token); // Get the first token from the input
genpreamble(); // Output the preamble
statements(); // Parse the statements in the input
genpostamble(); // Output the postamble
fclose(Outfile); // Close the output file and exit
exit(0);尝试一下
新的和更改的代码就是这样。让我们尝试一下新代码。这是新的输入文件input01:
print 12 * 3;
print
18 - 2
* 4; print
1 + 2 +
9 - 5/2 + 3*5;
是的,我决定检查一下我们是否有分布在多条线上的代币。要编译并运行输入文件,请执行以下操作make test:
$ make test
cc -o comp1 -g cg.c expr.c gen.c main.c misc.c scan.c stmt.c tree.c
./comp1 input01
cc -o out out.s
./out
36
10
25它有效!
结论和下一步是什么
我们已经在我们的语言中添加了第一个“真正的”语句语法。我已经用 BNF 表示法定义了它,但使用循环而不是递归来实现它更容易。别担心,我们很快就会回去进行递归解析。
在此过程中,我们必须修改扫描器,添加对关键字和标识符的支持,并更清晰地分离通用代码生成器和特定于 CPU 的生成器。
在编译器编写之旅的下一部分中,我们将向语言添加变量。这将需要大量的工作。下一步