cucu: a compiler you can understand (part 1)

译者序:

最近在学习一些编译器的基本知识,就找到了这篇英文的博客,在csdn搜了一下貌似没有人翻译,所以我干脆翻译了算了,反正都是学习。

原文地址:http://zserge.com/blog/cucu-part1.html

cucu: 一个易于理解的编译器 (part 1)

让我们来讨论一下编译器吧。你有想过自己去写一个编译器吗?

我将会让你看到这是一件多么简单的事情!但这个博客的第一部分有点偏理论,所以希望你们能够保持耐心。

我们的目标

CUCU 是一个“玩具”编译器用来编译一个“玩具”语言。我希望这个玩具语言能尽可能的像标准C语言,因此一个正确的CUCU程序同样能够使用C编译器进行编译。当然,整个C语言标准时非常复杂的,我们这里的CUCU所使用的语法只是C语言的一小部分。

比如说,这里有一个有效的CUCU程序片段:

int cucu_strlen(char *s) {
    int i = 0;
    while (s[i]) {
        i = i + 1;
    }
    return i;
}

语法

接下来我们要定义我们这个编程语言的语法。这是一个重要的步骤,因为在设计我们编译器的时候将会依赖于这个语法。

让我们从上到下来设计语法。我们的源文件包含一个程序。什么是程序?根据经验我们可以知道,程序就是一个列表,包括变量声明、函数声明、函数定义等,比如:

int func(char *s, int len); /* function declaration */
int i;                      /* variable declaration */

int func(char *s, int len) { /* function definition */
    ...
}

让我们尝试着将它写成EBNF的形式(如果你不知道什么是EBNF也没关系,它看上去很直观):

(译者:关于EBNF的详细信息请参考http://zh.wikipedia.org/wiki/%E6%89%A9%E5%B1%95%E5%B7%B4%E7%A7%91%E6%96%AF%E8%8C%83%E5%BC%8F)

<program> ::= { <var-decl> | <func-decl> | <func-def> } ;

这个表示法说明:一个函数是一个重复的序列,这个序列中的每一项是变量声明、函数声明或者函数定义。那么,这些所谓的声明和定义又是啥呢?让我们继续往下走。

<var-decl> ::= <type> <ident> ";"
<func-decl> ::= <type> <ident> "(" <func-args> ")" ";"
<func-def> ::= <type> <ident> "(" <func-args> ")" <func-body>
<func-args> ::= { <type> <ident> "," }
<type> ::= "int" | "char *"

因此,变量声明很简单:一个类型名加上一个标识符,然后在后面加上一个分号,就像我们经常在C语言中使用的那样。

int i;
char *s;

函数声明稍微要复杂一点,首先是“类型+标识符”,然后在括号里可以有选择性的加上 <func-args>

函数的参数表,是一个用逗号分割开的“类型+标识符”的序列,比如:

char *s, int from, int to

事实上,参数表最后的逗号在CUCU语言里是允许的,但不是必要的。之所以这么做是为了使我们分析代码变的简单。

语言所支持的类型只有int和char*,标识符是一串字母、数字或者下划线。

唯一没有说明的只有<func-body>. 但首先我们需要讨论一下语句(statements)和表达式(experssions)。

语句是指我们的语言中最小的独立元素。下面是一下有效的语句:

/* 这是一些简单的语句 */
i = 2 + 3; /* 赋值语句 */
my_func(i); /* 函数调用语句 */
return i; /*返回语句 */

/* 这是一些复合语句 */
if (x > 0) { .. } else { .. }
while (x > 0) { .. }

表达式是语句的一部分,它比语句更小。和语句不同的是,表达式总是会返回一个值。通常,表达式会是算数预算。比如在语句func(x[2], i + j)里,表达式是 x[2] 和 i+j

因此根据上述分析,我们有:

<func-body> ::= <statement>
<statement> ::= "{" { <statement> } "}"                /* 语句块 */
                | [<type>] <ident> [ "=" <expr> ] ";"  /* 赋值 */
                | "return" <expr> ";"
                | "if" "(" <expr> ")" <statement> [ "else" <statement> ]
                | "while" "(" <expr> ")" <statement>
                | <expr> ";"

下面是一些CUCU语言中可行的表达式:

<expr> ::= <bitwise-expr> 
           | <bitwise-expr> = <expr>
<bitwise-expr> ::= <eq-expr>
                   | <bitwise-expr> & <eq-expr>
                   | <bitwise-expr> | <eq-expr>
<eq-expr> ::= <rel-expr>
              | <eq-expr> == <rel-expr>
              | <eq-expr> != <rel-expr>
<rel-expr> ::= <shift-expr>
               | <rel-expr> < <shift-expr>
<shift-expr> ::= <add-expr>
                 | <shift-expr> << <add-expr>
                 | <shift-expr> >> <add-expr>
<add-expr> ::= <postfix-expr>
               | <add-expr> + <postfix-expr>
               | <add-expr> - <postfix-expr>
<postfix-expr> ::= <prim-expr>
                   | <postfix-expr> [ <expr> ]
                   | <postfix-expr> ( <expr> { "," <expr> } )
<prim-expr> := <number> | <ident> | <string> | "(" <expr> ")"

注意到递归定义的表达式了吗?除此之外这些表达式还说明了运算符的优先级,从下到上优先级以此降低:括号和方括号的优先级较高,而赋值的优先级较低。

例如,根据语法定义,表达式 8>>1+1 的运算顺序将会是  8>>(1+1)), 而不会是 (like in (8>>1)+1), 因为 >> 的优先级要低于 +.

词法分析器

当我们解决了语法问题,我们差不多可以开始了。第一件事是做一个词法分析器。我们的编译器使用一个字节流作为输入,而词法分析器的作用就是将这个字节流分割成更小的符号(token),以便于后续的处理。词法分析器为我们提供了某种程度的抽象使得之后的解析器得以简化。

例如,一个字节序列 "int i = 2+31;"将会分成以下的符号:

int
i
=
2
+
31
;

在一个普通的词法分析器中,一个词素是一个由类型和值组成的二元组。因此,相对于以上的列表,我们更期望能得到一个如下的二元组<TYPE:int>,<ID:i>, <ASSIGN:=>,<NUM:2>,<PLUS:+>,<NUM:31>,<SEMI:;>为了简便我们现在是通过值来反推类型,当然这是非常不严谨的。

词法分析器的主要问题是一旦一个字节从流中读取了之后,它就再也不能被重新放回流中。因此,如果我们读到了一个字节,而这个字节不能被加入到当前的符号中,这时候应该怎么办呢?我们应当把这个字节存到哪里,等待当前符号处理完成之后再去处理这个字节呢?

事实上,几乎任何词法解析器都有预读的机制。我们的语法很简单,因此我们只需要一个字节 - nextc当缓冲区就足够了。它存储一个从流中读取出来的但还没有被加入到当前符号中的字节。

另外,我必须在这里提个醒- 我在CUCU的代码的词法分析器中使用了很多全局变量。我知道这是个不好的习惯,但如果我把所有的变量都作为函数参数的话,这个编译器的代码看起来就不是那么的简洁了。

词法解析器的全部就是一个函数 readtok() 。而它的算法也很简单:

  • 跳过开头的所有空格
  • 尝试读取一个标识符(一个字母、数字以及下划线的序列)
  • 如果发现不是一个标识符,尝试读取一些运算符,比如 &, |, <, >, =, !.
  • 如果不是运算符,尝试读取字符串文本,比如"...." 或者 '....'
  • 如果仍然失败,或许是一个注释,比如 /* ... */
  • 如果继续失败,尝试读取一个字节,或许是括号之类的字符,比如 "("或者 "["。

    #include <stdio.h> /* for vpritnf */
    #include <stdarg.h> /* for va_list */
    #include <stdlib.h> /* for exit() */
    #include <ctype.h> /* for isspace, isalpha... */
    
    #define MAXTOKSZ 256
    static FILE *f; /* input file */
    static char tok[MAXTOKSZ];
    static int tokpos;
    static int nextc;
    
    void readchr() {
        if (tokpos == MAXTOKSZ - 1) {
            tok[tokpos] = '\0';
            fprintf(stderr, "token too long: %s\n", tok);
            exit(EXIT_FAILURE);
        }
        tok[tokpos++] = nextc;
        nextc = fgetc(f);
    }
    
    void readtok() {
        for (;;) {
            while (isspace(nextc)) {
                nextc = fgetc(f);
            }
            tokpos = 0;
            while(isalnum(nextc) || nextc == '_') {
                readchr();
            }
            if (tokpos == 0) {
                while (nextc == '<' || nextc == '=' || nextc == '>'
                        || nextc == '!' || nextc == '&' || nextc == '|') {
                    readchr();
                }
            }
            if (tokpos == 0) {
                if (nextc == '\'' || nextc == '"') {
                    char c = nextc;
                    readchr();
                    while (nextc != c) {
                        readchr();
                    }
                    readchr();
                } else if (nextc == '/') {
                    readchr();
                    if (nextc == '*') {
                        nextc = fgetc(f);
                        while (nextc != '/') {
                            while (nextc != '*') {
                                nextc = fgetc(f);
                            }
                            nextc = fgetc(f);
                        }
                        nextc = fgetc(f);
                    }
                } else if (nextc != EOF) {
                    readchr();
                }
            }
            break;
        }
        tok[tokpos] = '\0';
    }
    
    int main() {
        f = stdin;
        nextc = fgetc(f);
    
        for (;;) {
            readtok();
            printf("TOKEN: %s\n", tok);
            if (tok[0] == '\0') break;
        }
        return 0;
    }

    如果我们把一个C语言的源文件作为这个词法分析器的输入,它将会输出一个符号的列表,每个符号一行。

    搞定,让我们稍微休息一下,接着进入第二部分。



     
  • 0
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
旋转变压器---数字转换器作为现代伺服系统中被广泛使用的角位置测量系统,大量应用于高精度及大中型数控系统、机器人控制、工业控制、武器火力控制及惯性导航领域中。 传统的角测量系统面临的问题有:体积、重量、功耗偏大,调试、误差补偿试验复杂,费用较高。本文从微型化、智能化的方向进行研究,是解决传统角测量系统所面临问题的好途径。 本文所研究的旋转变压器---数字转换器是由信号调理模块、系统芯片C8051F064和输出控制模块组成的。整个系统的三路输入信号为X=AsinOcosar、Y=Acosθcos ot和Z=Ucosar(基准信号),输出信号为偏转角θ,输出形式为16 位数字量。信号调理模块是由模拟电路组成的,包括信号输入电路、相敏整流电路、滤波电路和直流稳压电源电路,其难点在于相敏整流电路的设计。信号调理模块的主要功能是把输入的交流信号X=AsinOcosor、Y=Acosθcosot转变成直流信号Bsinθ和Bcosθ,并使输出的直流信号在0~2.4V之间;系统芯片C8051F064是CYGNAL公司近年来推出的一款功能齐全的完全集成的混合信号片上系统型单片机。在本文所设计的系统中,系统芯片的输入信号为直流信号Bsinθ和Bcosθ,通过片内自带的2个16位A/D转换器对输入信号的数据进行采样和转换,并对转换完的数据进行滤波处理,以减小由于外界干扰而产生的误差,再用除法和反正切函数解算出偏转角θ的16位数字量;输出控制模块主要完成的功能是通过UARTO向计算机实时发送由单片机计算出来的偏转角度0的16位数字量,而串口的RS-232电平与单片机系统采用的是TTL电平之间的转换所采用的转换芯片是MC1488和MC1489。
将UC-Merced数据集分成训练集和测试集可以按照以下步骤进行: 1. 打开UC-Merced数据集所在的目录,将数据集中的所有图片按类别分好,例如,将所有airplane类别的图片放在一个名为airplane的文件夹中,将所有buildings类别的图片放在一个名为buildings的文件夹中,以此类推。 2. 在UC-Merced数据集所在的目录中创建一个名为train的文件夹和一个名为test的文件夹,用于存储训练集和测试集的数据。 3. 对于每个类别,将其中70%的图片随机抽取出来放入train文件夹中作为训练集,将其余30%的图片放入test文件夹中作为测试集。可以使用Python中的os和shutil模块来完成这个过程,具体的代码如下: ```python import os import random import shutil # 设置数据集目录和训练集、测试集目录 data_dir = '/path/to/UC-Merced' train_dir = os.path.join(data_dir, 'train') test_dir = os.path.join(data_dir, 'test') # 获取数据集中所有类别 classes = os.listdir(data_dir) # 遍历每个类别,将图片分成训练集和测试集 for cls in classes: cls_dir = os.path.join(data_dir, cls) train_cls_dir = os.path.join(train_dir, cls) test_cls_dir = os.path.join(test_dir, cls) os.makedirs(train_cls_dir, exist_ok=True) os.makedirs(test_cls_dir, exist_ok=True) # 获取该类别下的所有图片 images = os.listdir(cls_dir) # 随机打乱图片顺序 random.shuffle(images) # 计算训练集和测试集的分割点 split_point = int(0.7 * len(images)) # 将70%的图片放入训练集 for img in images[:split_point]: src_path = os.path.join(cls_dir, img) dst_path = os.path.join(train_cls_dir, img) shutil.copy(src_path, dst_path) # 将30%的图片放入测试集 for img in images[split_point:]: src_path = os.path.join(cls_dir, img) dst_path = os.path.join(test_cls_dir, img) shutil.copy(src_path, dst_path) ``` 以上代码会将UC-Merced数据集中的所有图片按照7:3的比例分成训练集和测试集,并将它们保存在train和test文件夹中。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值