【acwj】05,Statements 实现“Statements”

2023-05-16

搬运自https://github.com/DoctorWkt/acwj,一个介绍如何使用C语言编写一个可自举的类C语言编译器的说明。进行了粗略的翻译。

【acwj】05,Statements 实现“Statements”

It ‘s time to add some “proper” statements to the grammar of our language. I want to be able to write lines of code like this:

是时候在我们的语言语法中添加一些“适当的”语句了。我希望能够像这样编写代码:

   print 2 + 3 * 5;
   print 18 - 6/3 + 4*2;

Of course, as we are ignoring whitespace, there’ s no necessity that all the tokens for one statement are on the same line.Each statement starts with the keyword print and is terminated with a semicolon.So these are going to become new tokens in our language.

当然,由于我们忽略了空格,一条语句的所有标记不一定都在同一行。每条语句都以关键字print开头,以分号结尾。因此,这些将成为我们语言中的新标记。

BNF Description of the Grammar 语法的BNF描述

We’ve already seen the BNF notation for expressions. Now let’s define the BNF syntax for the above types of statements:

我们已经看到了expression的BNF表达,现在让我们为statements定义BNF语法

statements: statement
     | statement statements
     ;

statement: 'print' expression ';'
     ;

An input file consists of several statements. They are either one statement, or a statement followed by more statements. Each statement starts with the keyword print, then one expression, then a semicolon.

一个输入文件由几个语句组成。它们要么是一条语句,要么是一条语句后跟多条语句。每个语句都以关键字print开头,然后是一个表达式,然后是分号。

Changes to the Lexical Scanner 词法扫描器的更改

Before we can get to the code that parses the above syntax, we need to add a few more bits and pieces to the existing code. Let’s start with the lexical scanner.

在开始解析上述语法的代码之前,我们需要向现有代码中添加更多的片段。让我们从词法扫描器开始。

Adding a token for semicolons will be easy. Now, the print keyword. Later on, we’ll have many keywords in the language, plus identifiers for our variables, so we’ll need to add some code which helps us to deal with them.

为分号添加标记将很容易。现在,着重于print关键字。稍后,我们将在语言中有许多关键字,以及变量的标识符,因此我们需要添加一些代码来帮助我们处理它们。

In scan.c, I’ve added this code which I’ve borrowed from the SubC compiler. It reads in alphanumeric characters into a buffer until it hits a non-alphanumeric character.

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);
}

We also need a function to recognise keywords in the language. One way would be to have a list of keywords, and to walk the list and strcmp() each one against the buffer from scanident(). The code from SubC has an optimisation: match against the first letter before doing the strcmp(). This speeds up the comparison against dozens of keywords. Right now we don’t need this optimisation but I’ve put it in for later:

我们还需要一个函数来识别语言中的关键字。一种方法是使用一个关键字列表,并根据ScanIdentit()的缓冲区遍历列表和strcmp()。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);
}

Now, at the bottom of the switch statement in scan(), we add this code to recognise semicolons and keywords:

现在,在scan()中switch语句的底部,我们添加以下代码来识别分号和关键字:

    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);

I’ve also added a global Text buffer to store the keywords and identifiers:

我还添加了一个全局文本缓冲区Text来存储关键字和标识符:

#define TEXTLEN         512 // Length of symbols in input
extern_ char Text[TEXTLEN + 1];         // Last identifier scanned

Changes to the Expression Parser 表达式解析器的修改

Up to now our input files have contained just a single expression; therefore, in our Pratt parser code in binexpr() (in expr.c), we had this code to exit the parser:

到目前为止,我们的输入文件只包含一个表达式;因此,在binexpr()中的Pratt解析器代码(在expr.c中)中,我们用以下代码退出解析器:

  // If no tokens left, return just the left node
  tokentype = Token.token;
  if (tokentype == T_EOF)
    return (left);

With our new grammar, each expression is terminated by a semicolon. Thus, we need to change the code in the expression parser to spot the T_SEMI tokens and exit the expression parsing:

在我们的新语法中,每个表达式都以分号结尾。因此,我们需要更改表达式解析器中的代码,以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);
    }
}

Changes to the Code Generator 对代码生成器的修改

I want to keep the generic code generator in gen.c separate from the CPU-specific code in cg.c. That also means that the rest of the compiler should only ever call the functions in gen.c, and only gen.c should call the code in cg.c.

我想将gen.c中的通用代码生成器与cg.c中的CPU特定代码分开。这也意味着编译器的其余部分应该只调用gen.c中的函数,并且只有gen.c能调用cg.c中的代码。

To this end, I’ve defined some new “front-end” functions in gen.c:

在最后,我gen.c中定义了一些新的“前端”函数

void genpreamble()        { cgpreamble(); }
void genpostamble()       { cgpostamble(); }
void genfreeregs()        { freeall_registers(); }
void genprintint(int reg) { cgprintint(reg); }

Adding the Parser for Statements 为Statements添加解析器

We have a new file stmt.c. This will hold the parsing code for all the main statements in our language. Right now, we need to parse the BNF grammar for statements which I gave up above. This is done with this single function. I’ve converted the recursive definition into a loop:

我们有一个新文件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;
  }
}

In each loop, the code finds a T_PRINT token. It then calls binexpr() to parse the expression. Finally, it finds the T_SEMI token. If a T_EOF token follows, we break out of the loop.

在每个循环中,这段从T_PRINT标记开始寻找。然后,其调用binexpr()来解析该表达式。最后,它找到T_SEMItoken。如果最后跟随的是T_EOF标记,则我们跳出这个循环

After each expression tree, the code in gen.c is called to convert the tree into assembly code and to call the assembly printint() function to print out the final value.

在每个表达式树之后,调用gen.c中的代码将树转换为汇编代码,并调用汇编的printint()函数打印最终值。

Some Helper Functions 一些辅助函数

There are a couple of new helper functions in the above code, which I’ve put into a new file, misc.c:

上面的代码中有一对辅助函数,我将其放在了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, ";");
}

These form part of the syntax checking in the parser. Later on, I’ll add more short functions to call match() to make our syntax checking easier.

它们构成解析器中语法检查的一部分。稍后,我将添加更多的短函数来调用match(),以简化语法检查。

Changes to main() main函数的修改

main() used to call binexpr() directly to parse the single expression in the old input files. Now it does this:

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);

Trying It Out 试一试

That’s about it for the new and changed code. Let’s give the new code a whirl. Here is the new input file, input01:

print 12 * 3;
print 
   18 - 2
      * 4; print
1 + 2 +
  9 - 5/2 + 3*5;

Yes I’ve decided to check that we have have tokens spread out across multiple lines. To compile and run the input file, do a make test:

是的,我决定检查一下我们是否有分散在多行的标记。要编译并运行输入文件,请执行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

Conclusion and What’s Next 总结展望

We’ve added our first “real” statement grammar to our language. I’ve defined it in BNF notation, but it was easier to implement it with a loop and not recursively. Don’t worry, we’ll go back to doing recursive parsing soon.

Along the way we had to modify the scanner, add support for keywords and identifiers, and to more cleanly separate the generic code generator and the CPU-specific generator.

In the next part of our compiler writing journey, we will add variables to the language. This will require a significant amount of work.

我们已经在我们的语言中添加了第一个“真实”语句语法。我已经用BNF符号定义了它,但是用循环实现它更容易,而不是递归实现。别担心,我们很快就会回到递归解析。

在此过程中,我们必须修改扫描器,添加对关键字和标识符的支持,并更清晰地分离通用代码生成器和特定于CPU的生成器。

在编译器编写过程的下一部分中,我们将向语言中添加变量。这将需要大量的工作。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

【acwj】05,Statements 实现“Statements” 的相关文章

  • 抖音特效开放平台,点满我的短视频特效创作技能

    记得刚开始玩儿抖音的时候 xff0c 最喜欢看的就是技术流大拿们做的特效视频 xff0c 并且跃跃欲试想要加入他们 xff0c 奈何自己实在没有这么强的技术 再到后来 xff0c 抖音就推出了短视频特效道具 xff0c 我一直觉得 xff0
  • 怎么做视频特效?不妨试试抖音特效创作平台

    在这个信息满天飞的时代 xff0c 如何吸引用户主动的去接收信息对于内容生产来说至关重要 xff0c 从相关资料了解到 xff0c 视频是目前大家最喜欢的信息呈现方式 xff0c 一个有趣的视频可能会吸引成千上万用户的注意力 引爆整个网络场
  • 从0开始的视频特效制作之路

    随着短视频的火爆 xff0c 特效也随之火热了起来 作为短视频的重要玩法之一 xff0c 特效不仅降低了短视频拍摄制作的门槛 xff0c 还让用户的短视频形式丰富了起来 而最近爆火的 奶瓶面膜 视频特效 xff0c 更是给视频特效的出圈加了
  • 如何实现广告精准投放?一文获得新思路

    随着互联网人口红利的持续衰减 xff0c 互联网用户数量的增长速度越来越慢 市场进入存量 xff0c 用户们对产品质量要求越来越高 面对这样的市场阶段 xff0c APP开发者们做好广告精准投放是很有必要的 精准地广告投放在减少广告预算浪费
  • 激励视频广告——移动应用的财富密钥

    如何良好地平衡用户体验和用户增长 是广告行业的持久命题 xff0c 上网搜索 激励视频广告 你会发现类似的问题层出不穷 xff1a 请问什么是激励视频广告 xff1f 谁能麻烦介绍一下吗 xff1f 激励视频广告哪家做的好 xff1f 跪求
  • 设计模式详解:模式汇总与索引清单

    从本篇开始 xff0c 和您一起进入设计模式的世界 之前用C 做微信微信公众号开发系列文章 xff0c 更多的是原生模式 xff0c 帮助猿友们理解业务流程和基本实现方法 xff0c 但是那些类的实现仍然是用面向过程的思维方式 xff0c
  • 如何稳步实现互联网流量变现?

    我突然想起了自己刚做开发的时候 xff0c 那会还是菜鸟的我为了快速获取流量 最大程度的变现 xff0c 基本上不会考虑所谓的用户体验 xff0c 满脑子都是怎么引流 怎么变现 xff0c 所以常常引起用户反感 xff0c 严重折损了用户体
  • 一文获悉互动广告的投放攻略

    一直以来 xff0c 顶着 第四代互联网广告 头衔的 互动广告 通过互动 交流等方式进行双向传播 xff0c 一改以往传统广告的单向线性传播 xff0c 与此同时 xff0c 用户也从被动的观看者转变为主动的参与者 xff0c 直观地体验产

随机推荐

  • 穿山甲成长中心——人能尽其才则百事兴

    对于一众APP开发者来说 xff0c 要想在激烈的市场竞争中突出重围 xff0c 得到用户的青睐 xff0c 往往要面临重重困难 缺乏广告资源 xff0c 流量变现困难等着问题都使得开发者在刚刚进入市场时没有机会展现自己从而停滞不前 每一个
  • 广告精准投放的新出路为何?

    众所周知 xff0c 当前广告行业呈现普遍性的跨媒体投放 xff0c 测试成本 管理成本较高 xff0c 存在信息孤岛等情况 xff0c 一众广告主们通过数据分析优化投放的难度较大 在各类引擎中搜索 广告精准投放 xff0c 诸如 如何以更
  • 互联网流量变现出路为何?一文浅析

    国庆小长假在各大广告主和开发者们眼里就是一座巨大的流量富矿 xff0c 以国庆为主题推的广告和软件层出不穷 我突然想起了我自己刚做开发的时候 xff0c 那会还是菜鸟的我为了快速获取流量 最大程度的变现 xff0c 基本上不会考虑所谓的用户
  • MyBatisPlus 入门学习笔记(版本:3.4.3)

    文章目录 学习辅助资料 MyBatisPlus概述1 MyBatisPlus是什么2 特性 快速开始1 创建数据库 96 mybatis plus 96 2 导入相关依赖3 数据源配置3 快速开始3 1 User实体类编写3 2 mappe
  • Shell自动化脚本学习

    目录 xff08 1 6 xff09 Linux Shell脚本的自动化编程之shell xff1a 命令排序 xff08 1 7 xff09 Linux Shell脚本的自动化编程之shell xff1a 通配符 xff08 2 1 xf
  • ROS创建工作空间及功能包流程总结整理(python)

    ROS创建工作空间及功能包流程总结整理 xff08 python xff09 参考资料 xff1a B站赵虚左 xff1a https www bilibili com video BV1Ci4y1L7ZZ p 61 19 amp vd s
  • 中序计算式的计算器模型C语言实现

    span class token macro property span class token directive keyword include span span class token string lt stdio h gt sp
  • 设计模式详解:面向对象设计的七大原则

    单一职责原则 xff1a 一个对象应该只包含单一的职责 xff0c 并且该职责被完整地封装在一个类中 Single Responsibility Principle SRP Every object should have a single
  • 排序算法——猴子排序

    猴子排序 让一群猴子在打印机前昼夜不停地敲打键盘 xff0c 最终有可能能输入一部莎士比亚作品集 尽管概论微乎其微 同理 xff0c 把一堆扑克牌扔到天上 xff0c 等它们落下来的时候有概率会刚刚好从小到大排成一列 现在有一个无序的数组
  • ubuntu下firefox使用HTML 5播放器看B站

    ubuntu下FireFox使用HTML 5播放器看B站 firefox使用flash是真的难顶 xff0c 一直闪白 发现bilibili其实可以使用html 5播放器 使用 span class token function sudo
  • Ubuntu下将rm命令替换为trash命令

    Ubuntu下将rm命令替换为trash命令 rm命令是一个很可怕的命令 xff0c 因为它不会给你后悔的机会 xff0c 删了就是删了 xff0c 再也找不回来了 xff08 据说能在lost 43 found里面恢复 xff0c 但是操
  • 正则表达式_排除特定字符/字符串

    正则表达式 排除特定字符 字符串 使用场景 xff1a 使用git add A指令提交一个文件夹中所有的代码文件 xff0c 忽略所有的可执行文件 抽象化 匹配一些字符串 xff0c 找出其中不含后缀 xff0c 即 的字符串 理解 排除特
  • 什么是操作系统?操作系统的定义、功能、特性

    什么是操作系统 xff1f 操作系统的定义 功能 特性 什么是操作系统 xff1f 首先 xff0c 计算机的资源可以分为硬件资源和软件资源 CPU 存储设备 各种类型的输入输出设备与外设等 xff0c 共同构成计算机的硬件资源 各种程序
  • 计算机网络第六课

    计算机网络第六课 奈式准则未考虑噪声 噪声 xff1a 模拟信号 gt 数字信号转换 信道复用技术 xff08 续 xff09 在单一物理通信线路上 xff0c 传输若干个独立的信号 三种信道复用技术 xff1a FDMTDMWDM TDM
  • libc_hidden_def、libc_hidden_weak、libc_hidden_proto

    libc hidden def libc hidden weak libc hidden proto 在阅读glibc源码的时候 xff0c 遇见了几个没见过的宏 xff0c 几乎所有的函数都会使用这几个宏 xff1a libc hidde
  • GLIBC源码——putchar

    GLIBC源码 putchar GLIBC源码 从我认为最简单的putchar开始 putchar放在putchar c中 xff0c 而putchar c放在libio文件夹里 加上注释 xff0c 一共只有36行 span class
  • 【汇编】正确使用IDIV指令

    汇编 正确使用IDIV指令 div为无符号除法 xff0c idiv为有符号除法 idiv进行的是128 64位除法 xff0c 即被除数为128位 除数为64位 64位操作系统中寄存器大小当然只有64位 xff0c 因此 xff0c id
  • 【acwj】04,An Actual Compiler 一个真正的编译器

    搬运自https github com DoctorWkt acwj xff0c 一个介绍如何使用C语言编写一个可自举的类C语言编译器的说明 进行了粗略的翻译 acwj 04 xff0c An Actual Compiler 一个真正的编译
  • 设计模式详解:工厂方法模式

    今天我们来看一下使用频率非常高的工厂方法模式 xff0c 看完原理分别给出 NET和JAVA两种语言的实现源码 定义 xff1a 工厂方法模式 xff1a 定义一个用于创建对象的接口 xff0c 但是 让子类决定将哪一个类实例化 工厂方法模
  • 【acwj】05,Statements 实现“Statements”

    搬运自https github com DoctorWkt acwj xff0c 一个介绍如何使用C语言编写一个可自举的类C语言编译器的说明 进行了粗略的翻译 acwj 05 xff0c Statements 实现 Statements I