编译器构造阅读笔记(2)
任何的编译器都必须要执行两个主要的任务,分析我们的源程序,以及综合机器语言。
同时,几乎所有的现代编译器都是语法制导的。
语法制导
第一个过程:词法分析(Lex 所做的工作)
编译过程中,由语法分析器识别的源程序的语法结构进行驱动,而语法分析其依靠 token (标记)创建结构
token 是语法的最低一级符号
第二个过程:语法分析(Yacc 所做的工作)
语法结构的识别是分析任务的主要部分,语义例程基于语法结构,实际提供程序的意义。
它们可以生成程序的某些中间表示(IR)或者直接生成目标代码。
值得注意的事情在于,如果生成的是中间表示,则它可以被优化器(optimizer)进行转换,以便于生成更加高效的机器语言程序。
给定一个形式语法规范(典型情况下,以上下文无关语法(CFG)的形式提供),语法分析器读入词法记号,并将它们按照所使用的 CFG 产生方式的规定分组为单元。
语法分析器典型情况下由表进行驱动。# 编译器构造阅读笔记(2)
任何的编译器都必须要执行两个主要的任务,分析我们的源程序,以及综合机器语言。
同时,几乎所有的现代编译器都是语法制导的。
语法制导
第一个过程:词法分析(Lex 所做的工作)
编译过程中,由语法分析器识别的源程序的语法结构进行驱动,而语法分析其依靠 token (标记)创建结构
token 是语法的最低一级符号
第二个过程:语法分析(Yacc 所做的工作)
语法结构的识别是分析任务的主要部分,语义例程基于语法结构,实际提供程序的意义。
它们可以生成程序的某些中间表示(IR)或者直接生成目标代码。
值得注意的事情在于,如果生成的是中间表示,则它可以被优化器(optimizer)进行转换,以便于生成更加高效的机器语言程序。
给定一个形式语法规范(典型情况下,以上下文无关语法(CFG)的形式提供),语法分析器读入词法记号,并将它们按照所使用的 CFG 产生方式的规定分组为单元。
语法分析器典型情况下由表进行驱动。
语义例程执行两种功能。
首先,它们检查每个结构的静态语义,也就是验证结果是合法和有意义的。
如果结构在语义上面是正确的,语义例程也就进行实际的翻译,也就是生成正确实现该结构的 IR 代码。
一遍编译器(one-pass)
一种简化了结构的编译器,将语义例程和代码生成部分合并,并且消除对于 IR 的使用。
比如,Pascal 编译器
小型的程序设计语言构造编译器 Micro,一个教学型语言,不能用于编写简单的程序。
非形式化定义 Micro.
形式化是一个数学上的术语,我们可以把他理解为对某样东西抽象化以后的造物
- 只有一种数据类型,整形
- 标识符采用隐式声明,长度不超过 32 个字符,标识符必须以字母开头,并且由字母、数字、下划线组成
- 文本常量是一串数字
- 注释只有一行,且由 – 开始
- 赋值语句
ID := Expression - begin, end, read, write 为保留字
- 词法记号不可以跨行
词法分析器从文本文件中读取源程序并产生记号表示流。事实上,在任意式可都不必有实际的流存在,因为词法分析器实际上是一个由语法分析器调用的函数,每调用一次,就产生一个记号表示。
由此产生出来的词法记号集合用 C语言 代码描述为
typedef enum token_types
{
BEGIN, END, READ, WRITE, ID, INTLITERAL,
LRAREN, RPAREN,SEMICOLON, COMMA, ASSIGNOP,
PLUSOP, MINUSOP, SCANEOF
}token;
extern token scanner(void);
老实说,这让我对 PGSQL 中 lex & yacc 中的部分代码,有了一个更加好的理解。
只能说,在不清楚原理的情况下,强行去做一样东西,只能是 “百思不得其解”
实战出英雄,这句话是对的,但是我们要到,把一样只能适用于一时一势的东西拿出来说,就难免会出谬误
词法分析器读入字符,并把他们组成词法记号。
把那段代码敲下来,加深记忆
#include <stdio.h>
#include <ctype.h>
int in_char, c; // 注意:int 是为了防止 char 类型不能承载的情况,比如 EOF(通常定义为 -1)
while ((in_char = getchar()) != EOF)// 从输入流中持续不断地读入字符
{
if (isspace(in_char)) // 如果是空格,就直接跳过
continue;
/*
ID :: = LETTER | ID LETTER
| ID DIGIT
| ID UNDERSCORE
*/
else if (isalpha(in_char)) // 如果是字母
{
for (c = getchar(); isalnum(c) || c == '_'; c = getchar())
;
ungetc(c, stdin); // 在结束循环之后,最后留下来的那个字符应当放回去(能够用这个字符结束循环,说明它既不是字母,也不是下划线)
return ID;
}
/*
INTLITERAL ::= DIGIT |
INTLITERAL DIGHT
*/
else if (isdigit(in_char)) // 如果是数字
{
while (isdigit(c = getchar()))
;
ungetc(c, stdin);
return INTLITERAL;
}
else // 既不是字母,也不是数字,就算作错误情况
{
lexical_error(in_char);
}
}
现在我算是理解为什么教科书不识别符号的原因了,因为它们想要更加细化地去识别符号。
一个值得注意的事情在于,标识符和保留字其实都是符合同一识别规范的,因此还需要引入更加深刻的东西来进行区分。
有两种普遍性使用的方法。
第一种方法中,词法分析器拥有一种保留字表,每当一个标识符被识别的时候,词法分析器都会检查保留字表,如果一个记号在这张表里面,那么它总是会被解释为保留字,而不是标识符。
在这里我想到了前面的循环,因为每次识别,都是一个个字符来的,换而言之,后面字符覆盖掉了前面字符的内容,这样就造成了数据的丢失。
解决的办法,应该是引入一个缓冲区,来做内容的保存。
第二种方法,保留字作为编译器符号表的初始部分,含有特殊的标记属性,词法分析器在识别一个标识符以后,在符号表中查找该标识符,如果发现有这项特殊的标记,就把他识别为关键字。
教科书之后的说法,验证了我之前提出来的东西,就是引入了缓冲区。
语义例程执行两种功能。
首先,它们检查每个结构的静态语义,也就是验证结果是合法和有意义的。
如果结构在语义上面是正确的,语义例程也就进行实际的翻译,也就是生成正确实现该结构的 IR 代码。
一遍编译器(one-pass)
一种简化了结构的编译器,将语义例程和代码生成部分合并,并且消除对于 IR 的使用。
比如,Pascal 编译器
小型的程序设计语言构造编译器 Micro,一个教学型语言,不能用于编写简单的程序。
非形式化定义 Micro.
形式化是一个数学上的术语,我们可以把他理解为对某样东西抽象化以后的造物
- 只有一种数据类型,整形
- 标识符采用隐式声明,长度不超过 32 个字符,标识符必须以字母开头,并且由字母、数字、下划线组成
- 文本常量是一串数字
- 注释只有一行,且由 – 开始
- 赋值语句
ID := Expression - begin, end, read, write 为保留字
- 词法记号不可以跨行
词法分析器从文本文件中读取源程序并产生记号表示流。事实上,在任意式可都不必有实际的流存在,因为词法分析器实际上是一个由语法分析器调用的函数,每调用一次,就产生一个记号表示。
由此产生出来的词法记号集合用 C语言 代码描述为
typedef enum token_types
{
BEGIN, END, READ, WRITE, ID, INTLITERAL,
LRAREN, RPAREN,SEMICOLON, COMMA, ASSIGNOP,
PLUSOP, MINUSOP, SCANEOF
}token;
extern token scanner(void);
老实说,这让我对 PGSQL 中 lex & yacc 中的部分代码,有了一个更加好的理解。
只能说,在不清楚原理的情况下,强行去做一样东西,只能是 “百思不得其解”
实战出英雄,这句话是对的,但是我们要到,把一样只能适用于一时一势的东西拿出来说,就难免会出谬误
词法分析器读入字符,并把他们组成词法记号。
把那段代码敲下来,加深记忆
#include <stdio.h>
#include <ctype.h>
int in_char, c; // 注意:int 是为了防止 char 类型不能承载的情况,比如 EOF(通常定义为 -1)
while ((in_char = getchar()) != EOF)// 从输入流中持续不断地读入字符
{
if (isspace(in_char)) // 如果是空格,就直接跳过
continue;
/*
ID :: = LETTER | ID LETTER
| ID DIGIT
| ID UNDERSCORE
*/
else if (isalpha(in_char)) // 如果是字母
{
for (c = getchar(); isalnum(c) || c == '_'; c = getchar())
;
ungetc(c, stdin); // 在结束循环之后,最后留下来的那个字符应当放回去(能够用这个字符结束循环,说明它既不是字母,也不是下划线)
return ID;
}
/*
INTLITERAL ::= DIGIT |
INTLITERAL DIGHT
*/
else if (isdigit(in_char)) // 如果是数字
{
while (isdigit(c = getchar()))
;
ungetc(c, stdin);
return INTLITERAL;
}
else // 既不是字母,也不是数字,就算作错误情况
{
lexical_error(in_char);
}
}
现在我算是理解为什么教科书不识别符号的原因了,因为它们想要更加细化地去识别符号。
一个值得注意的事情在于,标识符和保留字其实都是符合同一识别规范的,因此还需要引入更加深刻的东西来进行区分。
有两种普遍性使用的方法。
第一种方法中,词法分析器拥有一种保留字表,每当一个标识符被识别的时候,词法分析器都会检查保留字表,如果一个记号在这张表里面,那么它总是会被解释为保留字,而不是标识符。
在这里我想到了前面的循环,因为每次识别,都是一个个字符来的,换而言之,后面字符覆盖掉了前面字符的内容,这样就造成了数据的丢失。
解决的办法,应该是引入一个缓冲区,来做内容的保存。
第二种方法,保留字作为编译器符号表的初始部分,含有特殊的标记属性,词法分析器在识别一个标识符以后,在符号表中查找该标识符,如果发现有这项特殊的标记,就把他识别为关键字。
教科书之后的说法,验证了我之前提出来的东西,就是引入了缓冲区。