编译器构造阅读笔记(2)

113 阅读10分钟

编译器构造阅读笔记(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);
	}
}

现在我算是理解为什么教科书不识别符号的原因了,因为它们想要更加细化地去识别符号。

一个值得注意的事情在于,标识符和保留字其实都是符合同一识别规范的,因此还需要引入更加深刻的东西来进行区分。

有两种普遍性使用的方法。

第一种方法中,词法分析器拥有一种保留字表,每当一个标识符被识别的时候,词法分析器都会检查保留字表,如果一个记号在这张表里面,那么它总是会被解释为保留字,而不是标识符。

在这里我想到了前面的循环,因为每次识别,都是一个个字符来的,换而言之,后面字符覆盖掉了前面字符的内容,这样就造成了数据的丢失。

解决的办法,应该是引入一个缓冲区,来做内容的保存。

第二种方法,保留字作为编译器符号表的初始部分,含有特殊的标记属性,词法分析器在识别一个标识符以后,在符号表中查找该标识符,如果发现有这项特殊的标记,就把他识别为关键字。

在这里插入图片描述


教科书之后的说法,验证了我之前提出来的东西,就是引入了缓冲区。