目录

编译原理

2026-01-21

写在前面:

  • 本篇blog参考于 程序设计语言 编译原理 第3版 陈火旺等编著
  • 能看到前序当中指出 ,这本书参加的是 九五计划,超级无比老的一本书。
  • 知识 永远不会过时,但是其中的例子 略显陈旧。 例如 书中举例 使用了 上古人工智能编程语言 Lisp prolog fortran pascal 举例分析。
  • 参考了 南京大学软件工程编译原理 课程1-lexer-antlr-20240306_哔哩哔哩_bilibili

2 高级语言及其语法描述

2.1 程序语言的定义

2.1.1 语法

2.1.1.1 字母表

  1. 什么是高级语言 :任何语言程序都可看成是一定字符集(字母表)上的一字符串。

  2. 对于下面这样一个运算式

    0.5 * X1 + C
    

    在这个运算式当中,单词符号 包括“0.5” “*” “ X1” “ + ” “C” 。而表达式 “0.5 * X1+ C ” 称之为语言的一个 语法范畴语法单位

  3. 一个程序语言只使用一个有限字符集作为字母表

  4. 语法 包括 语法规则(产生规则) 以及 词法规则

2.1.1.2 词法规则

  1. 词法规则 :词法规则规定了字母表中 什么样的 字符串 是一个 单词符号

  2. 单词符号 一般包括 :各类型的 常数 、 标识符 、 基本字 、 算符 和 界符。

2.1.1.3 语法规则

  1. 语法规则 : 语法规则规定了如何从 单词符号 形成更大的 结构( **语法单位 ** )
  2. 工具 : 有限自动机 、 上下文无关文法
  3. 额外限定
    • 书写格式:fortran规定每行最多80列
    • 空白字符:多数情况下无意义。有时用作间隔符。

2.1.2 语义

  1. 语义规则 :对于一个语言,除了前文所述的 词法规则 和 语法规则 ,需要对于前文的 单词符号语法单位 的 意义 做出规定。

  2. 工具 :基于 属性文法 的 语法制导 翻译 方法 , 虽然这 还不是 一种 形式 系统 , 但它 还是 比较 接近 形式化的 。

  3. 程序 :描述一定数据的处理过程。

  4. 一个语言的层次可以大致如下 :

    flowchart TD
    A["程序"] 
    B["子程序 或 分程序"]
    C["语句"]
    D["表达式"]
    E["数据引用"]
    F["算符"]
    G["函数调用"]
       
    A --> B
    B --> C
    C --> D
    D --> E
    D --> F
    D --> G
       
    

2.2 高级语言的一般特性

2.2.1 高级语言的分类

请关注这样一个函数的实现,以区分不同类别语言的特性。

int fibonacci (int n)
{
    if (n == 1 ) return 1;
    if (n == 2 ) return 1 ;
    else return fibonacci ( n-1 )+ fibonacci ( n - 2 ) ;
}

2.2.1.1 强制式语言

被称为 过程式语言 例如 Fortran C Ada Pascal 都属于这类语言,前面的 内容 就是 这个范畴。

2.2.1.2 应用式语言

使用函数式的嵌套调用。

LISP ML 属于这个类别。

(defun fibonacci (n)
  (cond ((= n 1) 1)
        ((= n 2) 1)
        (t (+ (fibonacci (- n 1))
              (fibonacci (- n 2))))))
  • 其实能够看到,这个语言当中的运算符是被提前的 ,曾经在 形式语言与自动机 **这门课当中 有一道作业题已经证明了 这个语言是 **无歧义的
下面的文法生成的是具有x和y操作数、二元运算符+、-和* 的前缀表达式:E→+EE|* EE|-EE|x|y
  • b) 证明这个文法是无歧义的。

要证明文法 $G$ 是无歧义的,只需说明每个由 $G$ 生成的串 $w$ 都对应唯一的语法分析树。我们采用如下引理,并基于其进行证明。

引理 设 $w$ 是 $G$ 生成的任一串,定义函数 $f(i, w) = (\text{前 } i \text{ 个字符中操作符的个数}) - (\text{前 } i \text{ 个字符中运算数的个数}) + 1$,$0 \le i \le |w|$。 则对所有 $i$ 有:

  1. 若 $0 \le i < w $,则 $f(i, w) \ge 0$;
  2. $f( w , w) = 0$。

引理的证明 对 $w$ 的长度进行归纳。

  • 基始:当 $ w =1$ 时,$w$ 只能是 $x$ 或 $y$。此时 $f(0,w)=1$,$f(1,w)=0$,结论成立。
  • 归纳步骤:设 $ w >1$,则 $w$ 必以某个操作符 $op \in {+, -, *}$ 开头,且可写为 $w = op\;w_1 w_2$,其中 $w_1, w_2$ 也是 $G$ 生成的串(且长度均小于 $ w $)。根据归纳假设,引理对 $w_1, w_2$ 成立。

    考虑 $f(i,w)$ 的分段行为:

    1. 当 $i = 0$ 时,$f(0,w) = 1$。
    2. 当 $1 \le i \le 1+|w_1|$ 时,$f(i,w) = f(i-1, w_1) + 1$。 由归纳假设,$f(i-1, w_1) \ge 0$,故此时 $f(i,w) \ge 1 > 0$。
    3. 当 $1+ w_1 < i \le w $ 时,$f(i,w) = f\bigl(i - 1 - w_1 , \; w_2\bigr)$。
      由归纳假设,该值在 $i < w $ 时 $\ge 0$,且在 $i = w $ 时为 $0$。    
    综上,在 $i < w $ 时 $f(i,w) \ge 0$,且仅在 $i = w $ 时取 $0$。归纳完成。

利用引理证明文法无歧义 对任意由 $G$ 生成的串 $w$:

  • 若 $ w =1$,则 $w$ 只能是 $x$ 或 $y$,其语法分析树唯一。
  • 若 $|w|>1$,则 $w$ 必以操作符 $op$ 开头,故可写为 $w = op\,w’$。 根据引理,函数 $f(i,w)$ 在 $i=1$ 时值为 $2$,且随着 $i$ 增大,首次下降到 $1$ 的位置即为 $w_1$ 结束的位置(因为下降到 $1$ 意味着已读完一个完整的子表达式)。该位置由 $f(i,w)$ 的唯一性保证是唯一的,从而 $w$ 可被唯一地分解为 $op\,w_1 w_2$,其中 $w_1, w_2$ 均为 $G$ 生成的表达式。 由归纳假设,$w_1$ 与 $w_2$ 的语法分析树唯一,故整个串 $w$ 的语法分析树也唯一。

因此,文法 $G$ 是无歧义的。

2.2.1.3 基于规则的语言

例如 prolog 语言(上古人工智能编程语言)

实际上这个语言很像那个switch-case 分支语句,针对每一个条件给出一个输出。很适合写决策树噢!!!

% 规则:fibonacci(N, F) - N 是位置,F 是对应的斐波那契数
fibonacci(1, 1).
fibonacci(2, 1).
fibonacci(N, F) :-
    N > 2,
    N1 is N - 1,
    N2 is N - 2,
    fib(N1, F1),
    fib(N2, F2),
    F is F1 + F2.
    
% 调用示例
?- fibonacci(1, Result).  % Result = 1
?- fibonacci(2, Result).  % Result = 1  
?- fibonacci(5, Result).  % Result = 5
?- fibonacci(10, Result). % Result = 55

2.2.1.4 面向对象语言(牢坚!!!)

这个很熟悉了,给出一个足够面向对象的java语言。

public class fibonacci_obj {
    public static int fibonacci (int n){
        if (n <= 0) throw new IllegalArgumentException("n必须为正整数");
        else if (n == 1 || n == 2) return 1;
        else return fibonacci (n - 1) + fibonacci (n - 2);
    }
}

嗯,这很面向对象,哈哈我有对象啦!

2.2.2 程序结构

一个程序往往通过若干子程序段构造,书本采用了很多种语言讲述,我转述为c++完成。

2.2.2.1 多子程序结构

书本上用了fortran语言完成

void learn_in_tongji()
{
    learn_gaocheng();
    learn_oop();
}
int main ()
{
    learn_in_tongji();
    return 0;
}

2.2.2.2 子程序嵌套定义

原书用pascal举例

int main()
{
    double gpa = 5.0;
    if (gpa < 4.0){
        cout << "failed to get in school of computer science and technology" <<endl;
    } else {
        if (gpa <=5.0){
            cout << " sorry ,you can't graduate from tongji university!!!" << endl;
        }
        else {
            if (gpa <= 6.0){
                cout << "Not eligible for graduate school admission through recommendation" <<endl;
            } else{
                cout << "congradulations !"
            }
        }
    }
}

2.2.2.3 按包封装

原书用ada举例。其实是类似于结构体。

typedef struct tongji{
    int people = 3;
    int boat = 1;
    int oars = 3;
}; //不用怀疑,这是校徽

2.2.2.4 面向对象

原书用java举例

这样的语言真正实现了类之间的继承、多态、动态绑定等 特征。

class six{};
class five{};
class four{};
class seven{};
class two{};
class tongji:class six, class five, class four ,class seven ,class two{
};

2.2.3 数据类型与操作

一个数据类型通常包括以下三种要素:

  • 属性
  • 可以对这个数据对象的操作

2.2.3.1 初等数据类型

  1. 数值数据 : 整数、实数、复数
  2. 逻辑数据 : 逻辑型数据
  3. 字符数据 :字符、字符串
  4. 指针类型 : 地址

2.2.3.2 标识符与变量名

  • 标识符:由字母或数字组成的以字母为 开头的 一个 字符串 。在程序语言当中的 各种名字 都是由 各种标识符 表示 的。

  • 变量名:变量名相当于是 标识符的 一种,用于辅助程序员 标记 这个 变量 。

    • 类型 : 决定了能够取什么样的值,在计算机内的 表示方式 ,以及能够 施加什么 运算
    • 作用域 : 规定了值的存在范围。

2.2.3.3 基本数据结构

  • 数组:从0开始或者从1开始的数组。一个数组所需的存储空间的大小在编译的时候就知道,这叫做确定数组,否则称之为可变数组
  • 结构体
  • 字符串
  • 表格
  • 队列
  • 抽象数据类型,允许抽象运算。特征在于可以完成运算符重载。

2.2.4 语句与控制结构

2.2.4.1 表达式的形成规则

  1. 变量、常数是表达式

  2. 若$E_1,E_2$是表达式,$\theta$ 是一个二元运算符,则$E_1 \ \theta \ E_2$是表达式。

    事实上,这个表达式可以采用 前缀表达式 或者 后缀表达式 。

  3. 若E 是表达式,$\theta$ 是一个一元运算符, 则 $\theta \ E$ 或者 $E \ \theta$ 是表达式

  4. 若E 是表达式,则 $ ( E )$ 也是表达式。

一般情况下,运算符结合的优先性 与 按照 常识所学来 。

2.2.4.2 语句

  1. 赋值语句 a := b (这个叫海象运算符)。
    • 左值 一个名字 所指代变量的 地址
    • 右值 一个名字 所指代变量的 实际值
  2. 控制语句

    • 无条件转移语句 goto
    • 条件语句 if then
    • 循环语句 while repeat for
    • 过程调用语句 call
    • 返回语句 return
  3. 说明句

    其实这个就是类似于函数提前声明。

    // 1. 变量说明句:声明变量类型
    int age;          // 声明int型变量age
    float score = 90.5; // 声明+初始化float型变量score
       
    // 2. 函数说明句(函数原型):声明函数的参数、返回值
    int add(int a, int b); 
       
    // 3. 类型说明句:自定义类型
    typedef int Integer; // 声明Integer是int的别名
    struct Student {     // 声明结构体类型Student
        char name[20];
        int id;
    };
    
  4. 简单句和复合句

    就是把上面的语句复合起来用。例如

    if (...) goto ...
    

2.3 程序语言的语法描述

2.3.1 上下文无关文法

嗯,这是你形式语言当中学过的内容。不过我们来回顾一些概念吧。

  • 终结符号
  • 非终结符号
  • 开始符号
  • 产生式

  • 如果一个产生式,其中有如下这么多种情况的话,那么其中a、b、c、d分别称之为一个 候选式
P -> a|b|c|d
  • 其中 -> 读作 “定义为”,“ | ” 读作或 ,他们是 元语言符号

  • 若a -> b-> c -> d 那么这一串过程叫做从a到d的推导

  • $\alpha \overset{+}{\Rightarrow} \beta$ 表示至少经过1步推导。

  • $\alpha \overset{*}{\Rightarrow} \beta$ 表示至少经过0步推导。

  • $S \overset{*}{\Rightarrow} \alpha$ 其中$\alpha$称之为一个句型,如果$\alpha$当中仅含终结符号,那么这称之为一个句子。文法G产生的所有 句子 是一个 语言。记作 $L(G)$ 。$$ L(G) = { \alpha S \overset{+}{\Rightarrow} \alpha \& \alpha \in V^*}$$
  • 最左推导 每一步都对最左侧的非终结符推导,也即最右规约

2.3.2 语法分析树和歧义性

嗯,这里没有什么需要过多指出的。都是你学过的!\ o /

2.3.3 形式语言鸟瞰

  • 乔姆斯基 2026-01-12-cp_chomsky

  • 0 型文法,即图灵机。
  • 1 型文法,上下文有关文法。$ \alpha A \beta \rightarrow \alpha \gamma \beta $ 也就是说,对于A的替换必须要考虑A的上下文。
  • 2 型文法,上下文无关文法。不用考虑呗。
  • 3 型文法,右线性文法(左线性文法) ,也即正则文法。

3 词法分析

3.1 对于词法分析器的要求

3.1.1 词法分析器的要求

程序语言的单词符号一般分为以下几种:

  • 关键字 也就是c++当中的 保留字 。 例如:if while do protected
  • 标识符 int float 之类的
  • 常数 包括 整数、实型、布尔型、文字型等等
  • 运算符 + - * /
  • 界符 逗号,分号,括号

一个 词法分析器 输出的 单词符号 常常 表示成 如下的 二元式 :

(单词种别,单词符号的属性值)

其中:

  • 单词种别 通常用 整数编码 。如何确定分种的 方式,分种的 数量 ,分种的 编码方式 ,是重要且 困难的 技术性 问题。
    • 关键字:可以共同一种,也可以 一符一种 ,一符一种 更为方便
    • 标识符:一般统共一种
    • 常数:按照自己的类型分种
    • 运算符 :可以一符一种 , 也可以 进行分类 ,按照相似属性的 运算符 进行 分类。
    • 界符 :一般 一符 一种。

对于以下的c++代码,会被解析为:

while (i >=j ) i-- ;
单词种别 单词符号的属性值
while -
( -
id &i
>= -
id &j
) -
i &i
; -

3.1.2 词法分析器应当作为一个独立的子程序

  • 这样的程序能够让程序变得 更加调理
  • 有两种方式应用这个程序
    • 将代码转化成为一个中间文件 保存。
    • 每次需要解析的时候 调用这个 子程序。

3.2 词法分析器的设计

  • 输入:程序文本、字符串+词法单元(token)的规约
  • 输出:词法单元流
  • 词法分析器生成器(antlr)/手写词法分析器/自动化词法分析器(自己实现一个词法分析器生成器)
  • 手写的工作量并不大,词法分析器对于错误处理并不友好

3.2.1输入 预处理

  • 预处理谁?空白符,跳格符,回车符,换行符

3.2.2 超前搜索

将需要处理的内容预先读入缓存区当中,在缓冲区中对于一个个的字段进行超前搜索。

需要识别:关键字,标识符,常数,算符和界符。

3.3 正规表达式与有限自动机

3.4 词法分析器的自动产生

3.4.1 语言lex的一般表述

使用antlr4 语言完成了如下内容

grammar CLexer ;

prog : stat* EOF;

stat : expr SEMI
     | ID ASSIGN expr SEMI
     | PRINT  expr SEMI
     ;
ASSIGN : '=' ;
MUL : '*' ;
DIV : '/' ;
ADD : '+' ;
SUB : '-' ;
LPAREN : '(' ;
RPAREN : ')' ;
PRINT : 'print' ;
SEMI : ';' ;
expr : expr MUL expr
    | expr DIV expr
    | expr ADD expr
    | expr SUB expr
    | LPAREN expr RPAREN
    | ID
    | INT
    ;
// 显然的, 如果不出现这样的递归调用的话, 将会产生一个有穷语言
ID : ('_' |LETTER)('_' | LETTER | DIGIT )*;

INT : '0' | ('+' | '-' )? [1-9]DIGIT *;
//                      ^ 此处问号表示前面的符号可有可无, 也就是说整数可以以正负号开头, 也可以没有符号开头

WS : [ \t\r\n]+ -> skip; //white space

DOCS_COMMENT : '/**' .*? '\n' -> skip; // documentation comment

SL_COMMENT : '//' .*? '\n' -> skip; // single line comment
                 // ^ ? 代表贪婪匹配后面的内容,也就是遇到第一个后面的内容就结束

SL_COMMENT_2 : '//' (~[\n]*) '\n' -> skip; // single line comment alternative
                 //  ^ ~ 表示匹配除了\n 以外的所有内容,直到遇到 \n
ML_COMMENT : '/*' .*? '*/' -> skip; // multi-line comment
                 // ^ ? 起到了同样的作用

fragment LETTER : [a-zA-Z] ;
fragment DIGIT : [0-9] ;


语法规则以小写字母开头, 词法规则以大写字母开头

antlr4 有如下几种特殊的匹配规则:

  • 最前优先匹配——更靠前的语法规则会被匹配
  • 最长优先匹配——更长的词法规则会被匹配
  • 非贪婪匹配——()?? () * ? ,()+ ?

测试内容:

//a= a + b;

//a = a * b;

a = 3;
print a;

/* 12\
3 */
/** 123 **/

1+ 2 * 3 / 4 ;


一个期望的结果如下

parseTree

3.4.2 超前搜索

所谓超前搜索,就是在文档中找到匹配当前词法单元最长的字符串

3.4.3 LEX 的实现

在课程中遇到了一个java关键字 final 。这里特别说明

特性 C++ const C++ constexpr Java final(对比参考)
核心语义 只读(运行时 / 编译期常量均可) 编译期常量(必须编译期确定值) 不可重赋值(编译期 / 运行时常量均可)
初始化要求 可以用运行时的值初始化(如函数返回值)。例:int getNum() { return 10; }const int a = getNum(); // 合法,运行时常量 必须用编译期可计算的值初始化,不能用运行时数据。例:constexpr int a = getNum(); // 编译错误(getNum 不是 constexpr 函数)constexpr int b = 10 + 20; // 合法(编译期计算) 可以用运行时的值初始化(如构造器中赋值)。例:final int a = new Random().nextInt(); // 合法
适用场景 修饰运行时只读的变量(如函数参数、类成员)。 修饰必须编译期确定的常量(如数组大小、模板参数)。 修饰运行时不可重赋值的变量,仅 static final 基本类型+字面量 是编译期常量。
  public Token(TokenType type, String text) {
    this.type = type;
    this.text = text;
  }

这里体现了前文所述,一个词法单元可以解析为 类型和 内容

类型大致如下

package dragon;

/**
 * Types of tokens
 * Grouped by hardness of recognition
 */
public enum TokenType {
  // Group 0
  EOF,  // end of file
  UNKNOWN,  // for error

  // Group 1
  // lookhead = 1 (LA(1))
  DOT, POS, NEG,
  IF, ELSE,
  ID,
  INT,
  WS,

  // Group 2
  // =, <>, <, <=, >, >=
  // LA(2) lookahead = 2 2位超前搜索
  EQ, NE, LT, LE, GT, GE,

  // Group 3
  // arbitrary LA 需要向前看任意长度的文本
  REAL,// integer double
  SCI,// scientific notation 科学计数法
}

对于这样一个语法单元123Ex, 前面 读取到E之后 猜测 这是一个词法单元,但是这是一个错误的猜测,就要回退到上一个正确的地方。

一个正确的lexer 应该做到 记录来时最长匹配,无路可走便回头

3.4.3.1int id ws

对于这三类词法单元,需要不断重复直到遇到第一个错误

3.4.3.2 运算符号

需要一个 有限DFA便可识别出所有需要的内容。