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积分下载,若非0积分请勿轻易下载 亲爱的CSDN用户: 首先感谢你点进这个资源页面。我需要提前说明一个重要情况: 本资源原本已设置为“0积分下载”,即作者希望完全免费共享。但CSDN平台有时会根据文件的下载热度、文件大小、用户权限等因素,自动将部分资源的积分调整为非0数值(如1积分、2积分、5积分等)。这是平台系统的自动行为,而非作者本人的设定。 因此,如果你当前看到该资源的下载所需积分不是0(例如显示为1、2、3……),请谨慎决定是否下载。 如果你按照非0积分支付并下载后发现资源内容不符合预期、链接失效,或者实际上该资源本应是免费的,作者无法为此承担积分损失或退还操作。强烈建议:仅在页面显示为0积分时进行下载。 另外,本资源描述中并未直接提供具体的下载地址或外部链接,因为它本身是一个通过CSDN官方上传通道提交的文件/内容包。如果你看到描述中没有外部网盘地址,这是正常的——资源文件应通过CSDN内置的“下载”按钮获取。若因平台积分显示异常导致你支付了积分,请优先联系CSDN客服咨询积分退还政策,作者没有权限修改平台自动设定的积分值。 感谢你的理解与支持。技术分享本应开放,但受限于平台规则,特此提醒如上。祝学习进步!
源码链接: https://pan.quark.cn/s/064420f76eb8 ### A2L文件制作教程与规范 ### #### 一、引言 在汽车电子领域,A2L文件是一种用于阐释电子控制单元(ECU)测量与校准数据的标准格式。该格式依据ASAP2(Automotive Standard Input Output Bus Protocol for Parameter Access)标准进行定义,并在电子控制单元的开发、测试及诊断环节中得到广泛运用。本指南将系统性地介绍A2L文件的编制流程及其遵循的规范,旨在为工程师群体提供具有实践价值的指导。 #### 二、A2L文件基础知识 1. **定义**:A2L文件是一种基于ASCII码的文本性载体,主要功能是存储电子控制单元内所有可测量及可校准对象的详细信息。 2. **作用**: - **参数管理**:系统性地记录电子控制单元中的参数配置详情。 - **诊断支持**:为故障诊断提供必要的数据支撑,包括故障代码的读取等操作。 - **软件开发**:在软件开发阶段,对参数配置进行辅助性管理。 3. **组成结构**: - **头部信息**:涵盖文件版本号、生成日期等基础性信息。 - **模块定义**:将每个电子控制单元设定为一个独立的模块进行详细描述。 - **测量点和校准通道**:明确电子控制单元内部测量点与校准通道的具体设置。 - **特征描述**:对电子控制单元的特定性能进行说明,例如温度传感器的性能曲线。 #### 三、A2L文件制作工具 - **ASAP2Editor**:由Vector Informatik GmbH开发的一款专业级工具,专门用于A2L...
内容概要:本文系统介绍了物理信息神经网络(PINNs)在求解布洛赫-托雷(Bloch-Torrey)方程中的具体应用,并提供了基于PyTorch框架的Python代码实现案例。研究通过将物理先验知识嵌入神经网络的损失函数中,结合深度学习方法高效求解复杂的偏微分方程,充分展现了PINNs在科学计算与工程仿真领域的优越性。文章详细阐述了模型架构设计、物理约束的数学表达、网络训练流程以及数值实验结果分析,突出了数据驱动方法与物理机理深度融合的研究范式,为相关领域的复杂系统建模提供了新的技术路径。; 适合人群:具备一定深度学习理论基础,熟练掌握PyTorch框架,从事科学计算、生物医学工程、数值模拟或物理建模等相关领域研究的研究生、科研人员及工程师。; 使用场景及目标:①深入理解物理信息神经网络(PINNs)的核心原理及其在偏微分方程求解中的具体实现方法;②掌握如何将物理定律(如扩散方程)转化为神经网络可优化的损失项;③复现并拓展该方法至扩散磁共振成像(dMRI)、材料科学等涉及布洛赫-托雷方程的实际物理系统仿真研究; 阅读建议:建议读者结合所提供的完整代码进行动手实践,重点关注损失函数的设计、初始/边界条件的施加方式以及超参数调优策略,并尝试将该框架迁移应用于其他类型的物理系统建模问题中,以深化对物理引导机器学习的理解。
内容概要:本文系统阐述了利用物理信息神经网络(PINNs)结合PyTorch框架求解欧拉-伯努利(Euler-Bernoulli)双梁正问题的完整技术路线,通过Python代码实现了对双梁结构在特定载荷作用下的变形与应力分布的高精度数值建模与求解。该方法深度融合深度学习与物理守恒定律,将控制微分方程作为先验知识嵌入神经网络的损失函数中,有效克服了传统数值方法对网格划分和大量标注数据的依赖。文中详尽展示了神经网络架构设计、边界与初始条件的数学表达与代码实现、物理约束项构造、复合损失函数优化策略及训练收敛过程,并通过对比分析验证了PINNs在固体力学正问题求解中的准确性、鲁棒性与泛化潜力。; 适合人群:具备扎实的高等数学、弹性力学和偏微分方程基础,熟悉深度学习基本原理与PyTorch框架编程,从事计算力学、工程仿真、数据驱动建模等领域研究的研究生、科研人员及高级工程师;特别适合致力于探索AI for Science、开发新一代无网格计算方法的研究者。; 使用场景及目标:①为复杂工程结构(如桥梁、建筑框架)的动力学响应分析提供一种高效的替代仿真手段,显著降低计算成本;②推动物理信息驱动的人工智能模型在航空航天、土木工程等领域的实际应用,提升多物理场耦合问题的求解效率;③为后续开展材料参数反演、损伤识别、结构健康监测等逆问题研究奠定坚实的理论与技术基础。; 阅读建议:建议读者结合文末提供的完整代码资源(可通过公众号“荔枝科研社”获取)进行动手实践,重点剖析物理控制方程与神经网络损失项之间的映射关系,尝试调整网络深度、宽度、激活函数及优化器参数以探究其对求解精度与收敛速度的影响,从而深刻理解PINNs的核心思想与工程实现细节。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值