词法分析的最终目标是将输入字符流变成一个个符号,因此还需要引入一个新的结构:
/* datastruct.h */
struct Token {
int line; // 所在行
AcceptType type; // 类型
char* image; // 像
};
其中type和image域是至关重要的,分别表示该符号的接受类型和"外貌",后者对于常数值、标识符很重要,而对于关键字和运算符,实际上知道其类型就行了。而所在行line域则是调试和报错时需要用到。
为了不使解析过程过于复杂,并且考虑到设计应该尽可能的模块化,实现中将产生符号和获取字符的部分抽象成为接口:
/* dfa.c */
int nextChar(void); // 获取下一个字符
struct Token* firstToken(void); // 获取第一个符号指针,这个特殊的调用可能会包含某些初始化
struct Token* nextToken(void); // 获取下一个符号指针
void eofToken(void); // 表示分析过程结束
步交替方法
如果输入仅仅是一个串的话,那么判别DFA是否接受它很简单。然而现在输入是一连串等待分割的符号,因此当分析过程产生错误时,不应该是立即报错,而应该把从出错开始的部分试图拼凑到后一个符号中,如输入
123.45+a
首先自动机会一直识别到"123.45",接下来看到"+",出错,这时就应该恢复最后一次成功接受的状态(即读入了"123.45"时),然后从"+"开始重新识别。这里涉及到两点,一是记录状态,另一是缓存字符。Jerry语言的词法很简单,实际上,只需要缓存一个字符就可以了,而相应地,状态只需要记录到上一状态即可(可以认为输入"123.45"和"123.45+"这两个字符串的状态是连续的两个状态,当下一状态出错时,恢复上一状态就可以了)。对于此,这样一些变量就可以对付这些记录方式了:
struct State* state[2];
int sw = 0;
int character;
sw是一个转换变量,通过设置它来切换两个状态。 因为在出错时缓存了一个字符,所以当返回一个符号后并进入新一轮解析时,当前状态不应置为初始状态,而应该置为初始状态对该字符的跳转,因此循环体大致是这样的:
struct State* initial = initStates();
while(1) {
state[sw] = initial->nextState[character];
state[1 ^ sw] = NULL; // 将缓存字符追加到当前符号的image后面
while(NULL != state[sw]) {
character = nextChar();
if(EOF == character) {
if(DENY == state[sw]->type) {
// 报错
} else {
token->type = state[sw]->type;
}
free(initial);
eofToken();
return;
}
// 将当前字符追加到当前符号的image之后
state[1 ^ sw] = state[sw]->nextState[character];
sw ^= 1; // equivalent to "sw = 1 - sw;"
}
sw ^= 1;
if(NULL == state[sw]) {
// 报错
} else {
token->type = state[sw]->type;
token = nextToken();
}
}
然而开始读入第一个字符时,没有这样缓存过程,因此需要把这样一个过程单独抽取出来,放到循环之外——循环开头之前。 此外还有一个小细节,就是查询关键字表来确定一个标识符一样的东西是不是关键字,简单的实现是这样的:
static char* RESV_WORD_LIST[] = {
"", "else", "if", "while", "read", "write", "break", "int", "real",
NULL
};
static int foundAsResvWord(char* image)
{
int i = 1;
for(; NULL != RESV_WORD_LIST[i]; ++i) {
if(0 == strcmp(image, RESV_WORD_LIST[i])) {
return i;
}
}
return 0;
}
看起来很暴力的搜索,当然可以实现得更好一点,比如二分一下。需要注意的是,这里的各个关键字的顺序需要跟接受类型枚举中各个对应量的顺序一致,而标识符对应的量要在这些量之前——这个函数的返回值是一个偏移值,而不是接受类型枚举。大致的过程就是这样的,下面给出一个较完整的过程:
/* dfa.c */
void tokenize(void)
{
struct State* state[2];
int sw = 0;
int character;
char* image;
struct Token* token = firstToken();
struct State* initial = initStates();
character = nextChar();
// printf("--CHAR CODE-- %d %c", character, character);
if(EOF == character) {
exit(0);
}
/* 特别第一个读入的字符 */
while(1) {
state[sw] = initial->nextState[character];
state[1 ^ sw] = NULL;
image = token->image;
*(image++) = character;
while(NULL != state[sw]) {
character = nextChar();
// printf("--CHAR CODE-- %d %c ", character, character);
if(EOF == character) {
if(DENY == state[sw]->type) {
// 报错
} else {
token->type = state[sw]->type;
}
*image = 0;
free(initial);
eofToken();
return;
}
*(image++) = (char)(character & 0xff);
// *image = 0; printf("-- INFO -- %d %c %s\n", sw, character, token->image);
state[1 ^ sw] = state[sw]->nextState[character];
sw ^= 1; // equivalent to "sw = 1 - sw;"
}
sw ^= 1;
// printf("--RECONGIZED--\n");
if(NULL == state[sw]) {
// 报错
} else {
*(image - 1) = 0;
token->type = state[sw]->type;
if(IDENT == token->type) {
token->type += foundAsResvWord(token->image);
}
token = nextToken();
}
}
}
局限
步交替方法的局限归根结底,在于它仅能缓存一个字符和恢复到最近的一个状态。它在Jerry语言的词法分析构造中游刃有余并不表示它是一个通用的方法,假如有个3型文法接受长度为奇数,字符全为'a'的字符串或字符全为'b'的串,那么输入
aaaab
时,自动机读到第5个字符'b'时猛然发现前面有4个'a',因此不能接受,而需要向前回滚更多,对此,除非你构造一个如蜈蚣一样的步交替方法,否则词法分析无法继续。然而这种怪异的文法究竟有没有可能在实践中运用,可能永远是一个谜……
分享到:
相关推荐
以下是对词法分析和语法分析的详细解释: 1. **词法分析**: - 词法分析器,也称为扫描器或分词器,其主要任务是从源代码中识别出一个个有意义的单元,即“单词”或“标记”(tokens)。这些标记是编程语言的基本...
词法分析器的目的是识别和提取符合语法规则的最小可识别单元,为语法分析器提供输入,进而进行语法解析。 词法分析器的工作原理通常基于正则表达式或有限状态自动机。它会读取源代码的字符流,通过模式匹配来识别...
根据给定的信息,本文将对一个简单的C++词法分析程序进行详细的知识点解析,包括其功能、工作原理以及代码实现细节。 ### 一、词法分析程序简介 #### 标题解读:“词法分析程序 文本输入输出” 词法分析(Lexical...
词法分析器通过扫描源代码,识别并分类这些记号,为后续的语法分析提供输入。 在给定的代码片段中,词法分析部分主要通过`getWord`函数和`ignoreSpace`函数实现。`getWord`函数用于读取源代码字符串,并根据字符...
词法分析器的输入为 SQL 语言源代码,输出识别出单词的二元属性,填写符号表。单词符号的类型包括关键字、标识符、界符、运算符、整数、浮点数、字符串。 在词法分析的实现中我们分别实现了两套机制:手工构造词法...
在编程语言处理领域,语法分析器和词法分析器是至关重要的组成部分,它们主要用于解析源代码,将其转化为计算机可以理解的形式。在这个项目中,我们关注的是一个基于Java实现的语法分析器和词法分析器。Java是一种...
语法分析器接着词法分析器的工作,它负责解析由词法分析器生成的标记流,并根据语法规则构建抽象语法树(AST)。这一步骤是基于上下文无关文法(Context-Free Grammar,CFG)进行的,它定义了语言的结构和规则。例如...
6. **错误处理**:当遇到不符合词法规则的输入时,词法分析器需要能够适当地报告错误,比如非法字符、未闭合的字符串或注释,或者不匹配的运算符。 7. **实例应用**:在学习编译原理时,理解词法分析是构建自己的...
在Java中,我们可以使用正则表达式或者自定义的解析逻辑来实现词法分析器。例如,`WordThink3.java`可能就是一个实现了词法分析功能的类,它能够识别关键字、标识符、常量、运算符等不同的Token类型。 语法分析,紧...
2. 解析器:词法分析器也可以用于解析器,例如XML解析器、HTML解析器等。 3. 文本处理:词法分析器也可以用于文本处理,例如文本编辑器、文本检索等。 词法分析器是编译原理实验中的一个重要组件, Plays a crucial...
词法分析器是编译器设计中的重要组成部分,它的主要任务是将源代码文本转换成一个个有意义的符号或称为“标记”(Token),为语法分析阶段提供输入。在Java编程语言中,我们可以自定义实现一个词法分析器来处理特定...
3. **状态机构造**:基于词法规则,构建有限状态自动机( Finite State Automaton, FSA),它决定了词法分析器如何根据输入字符来决定当前应该识别的标记类型。 4. **标记生成**:当匹配到一个完整的模式时,词法...
词法分析器,也称为扫描器或词法分析器,主要任务是将源代码分解成一系列有意义的符号,这些符号被称为“词素”或“记号”,为后续的语法分析和语义分析阶段提供输入。 词法分析的目标是从源代码文本中识别出程序...
词法分析器,又称为扫描器或词法规则解析器,它的任务是从源代码中识别出符合特定语言语法的词汇元素,如关键字、标识符、常量、运算符等。这些元素根据语言规范被转化为记号,供后续的语法分析阶段使用。在C语言...
在Java语言中编写词法分析器,我们可以利用正则表达式库或者自定义的解析算法。正则表达式是定义词法规则的强大工具,能够方便地匹配各种字符模式。而自定义算法则可能涉及到状态机的设计,通过维护一系列的状态来...
一个简单的Java词法分析器可能会包含一个循环,不断地从输入缓冲区取出字符,根据当前状态和字符匹配正则表达式,更新状态,并可能生成记号。例如,处理数字时,从读取的第一个数字字符开始,直到遇到非数字字符为止...
总的来说,词法分析是解析源代码的第一步,它的正确实现对于编译器来说至关重要,因为它为后续的语法分析和语义分析提供了基础。通过这个实验,学生可以深入理解编译器的工作流程,并掌握如何利用编程语言处理实际的...
在C语言的上下文中,词法分析器的输出通常是生成一个标记流,这个流将被后续的语法分析阶段(解析器)处理,以构建抽象语法树(AST),进一步生成目标代码。因此,词法分析器的正确性和效率直接影响到整个编译过程的...
当遇到不符合规则的输入,如非法字符、未关闭的字符串、连续的标识符等,词法分析器应能生成错误消息,指出错误的位置和性质,帮助程序员定位并修复问题。 词法分析器的实现通常采用正则表达式或者有限状态自动机...
- **状态机**:词法分析通常涉及状态机的设计,以处理各种输入情况。在Rust中,这可以通过状态变量和匹配条件来实现。 - **解析器组合子**:`nom`库提供了许多函数,如`alt!`(选择)、`seq!`(序列)、`many!`...