Lex和Yacc(1) 入门

news/2026/1/17 23:30:15/文章来源:https://www.cnblogs.com/dewxin/p/19494431

本系列为deepseek机翻的《Lex&Yacc》


Lex和yacc可以帮你编写程序来解析结构化的输入。

在处理结构化输入(structured input)的程序中,有两个任务会反复出现:1.将输入转化为有意义的单元(unit)。2.找到这些单元之间的联系。

将输入划分为单元(也被叫做符号token)的过程被称为词法分析(lexical analysis)。 Lex可以通过输入一个描述tokens的集合来生成对应的C函数,这个函数被称为词法分析器(lexical analyzer)或者是lexer,亦或者是scanner,反正意思就是能够识别token的东西。

这个用于描述tokens的集合被称为 lex规范(lex specification)。lex用正则表达式(regular expressions)来描述token。

当输入被转化为token符号后,程序接下来就需要建立这些token之间的联系。 这个任务被称作解析(parsing),而用于定义关系的规则就是语法(grammar)。

Yacc 可以通过精确定义的语法产生一个C语言函数来解析这样的语法,这样的函数又被称为解析器(parser)。

Yacc解析器能够自动检测输入符号流 何时匹配语法中的某条规则,同时也能在输入与任何规则都不匹配时检测出语法错误。

使用Lex识别单词

让我们构建一个能识别不同类型英文单词的简单程序。我们首先从识别词性(名词、动词等)开始,后续将扩展该程序,使其能够处理符合简单英语语法的多词句子。

首先,我们来识别一下动词:

is am are were
was be being been
do does did will
would should can could
has have had go

Example 1-1 shows a simple lex specification to recognize these verbs.

Example 1-1. Word recognizer ch1-02.l

%{
/*
* this sample demonstrates (very) simple recognition:
* a verb/not a verb.
*/
%}%%[\t ]+ /* ignore whitespace */ ;
is |
am |
are |
were |
was |
be |
being |
been |
do |
does |
did |
will |
would |
should |
can |
could |
has |
have |
had |
go { printf("%s: is a verb\n", yytext); }
[a-zA-Z]+ { printf("%s: is not a verb\n", yytext); }
.|\n { ECHO; /* normal default anyway */ }%%main()
{
yylex() ;
}

我们将其保存为ch1-02.l,然后调用下面的shell代码。 在Linux中,我们可以使用flex代替lex。

% lex ch1-02.l
% cc lex.yy.c -o first -ll

Lex将词法规范转换为名为 lex.yy.c 的C源文件,我们将其与lex库 -ll 编译并链接。随后,执行生成的可执行程序以验证其是否符合预期,正如我们在本节前文所见。请尝试运行,您将确认这个简单的程序确实能准确识别我们预设的动词。
我们编译并调用这个程序后,会得到如下的结果。

% example1
did i have fun?
did: is a verb
I: is not a verb
have: is a verb
fun: is not a verb
?

为了解释其工作原理,让我们从第一部分开始:

%{
/*
* This sample demonstrates very simple recognition:
* a verb/not a verb.
*/
%}

第一部分,即定义部分(definition section) ,用于复制我们想要的C代码到最终程序中。

这一点尤其重要,例如,当我们需要包含某些头文件,以确保文件后续的代码能正常工作时。我们用特殊定界符%{%}将C代码包围起来。Lex会直接把%{%}之间的内容复制到生成的C文件中,因此您可以在此处编写任何有效的C代码。

%%标记着该部分的结束。

下一部分是规则部分(rule section)。每条规则由两部分组成:模式(pattern)和行为(action),两者之间用空白字符(whitespace, 空格或tab)分隔。Lex生成的词法分析器(lexer)在识别到模式时,将执行对应的行为。这些模式采用UNIX风格的正则表达式,与grep、sed和ed等工具使用的表达式类似,但略有扩展。第6章将详细说明正则表达式的所有规则。在我们的示例中,第一条规则如下: [\t ]+ /* ignore whitespace */ ;

方括号“[]”表示匹配括号内的任意一个字符。在此例中,我们接受“\t”(制表符)或“ ”(空格)。加号“+”表示模式匹配其前子模式的一个或多个连续重复。因此,该模式描述了空白字符(任意组合的制表符和空格)。这条规则的第二部分,即行为,仅是一个分号,这是一个无操作的C语句,其作用是忽略该输入。

下一组规则使用了“|”(竖线)。这是一种符号,表示使用与下一个模式相同的行为,因此所有动词都采用为最后一个模式指定的行为。

is |
am |
are |
were |
was |
be |
being |
been |
do |
does |
did |
should |
can |
could |
has |
have |
had |
go { printf("%s: is a verb\n", yytext); }

我们的模式会匹配列表中的任意动词。一旦识别出一个动词,我们就会执行行为——"一条C语言的printf语句"。字符串 yytext 包含与模式匹配的文本。该行为将输出被识别的动词,后接字符串“: is a verb\n”。

最后两个规则是:

[a-zA-Z]+ { printf("%s: is not a verb\n", yytext); }
.|\n { ECHO; /* normal default anyway */ }

模式 [a-zA-Z]+ 是一个常见的表达式:它表示至少包含一个字符的任意字母字符串。当在方括号内使用时,字符“-”具有特殊含义:它表示一个字符范围,从“-”左侧的字符开始,到其右侧的字符结束。当我们看到此类模式时,采取的行为是打印匹配到的词元及字符串“: is not a verb\n”。

很快我们就会发现,任何匹配前面所列动词规则的单词同样也会匹配这条规则。您可能会问:当它遇到列表中的动词时,为什么不会同时执行两个行为呢?此外,当遇到单词"island"时,由于它以"is"开头,是否会触发两个行为呢?答案是lex有一套简单的消歧规则。其中确保我们词法分析器正常工作的两条规则是:

  • Lex模式对给定的输入字符或字符串仅匹配一次。
  • Lex会为当前输入执行最长可能匹配对应的行为。因此,lex会将“island”视为匹配我们的all-inclusive规则,因为相比“is”,前者是更长的匹配。

最后一行是默认情况。特殊字符“.”(点号)匹配除换行符外的任意单个字符,而“\n”匹配换行符。特殊操作ECHO会将匹配到的模式输出,从而复制所有标点符号或其他字符。尽管这是默认行为,我们仍明确列出了这种情况。我们曾见过一些复杂的词法分析器因这一特性而工作异常,当默认模式匹配到意外输入字符时,会偶尔产生奇怪的输出。(尽管对于未匹配的输入字符存在默认操作,但编写良好的词法分析器总是会使用明确的规则来匹配所有可能的输入。)

规则部分由另一个 %% 作为结束分隔符。
最后一部分是用户函数(user subroutines)部分,可以包含任何合法的C代码。Lex会将其复制到生成的词法分析器代码之后。我们在此包含了一个 main() 程序。

%%
main()
{
yylex();
}

由lex生成的词法分析器是一个名为 yylex() 的C例程,因此我们调用它。除非操作中包含显式的返回语句,否则 yylex() 在处理完整个输入之前不会返回。

Example 1-2. Lex example with multiple parts of speech ch1-03.l

接下来 我们的第二个示例(示例1-2)会扩展词法分析器,使其能够识别不同的词性。
Example 1-2. Lex example with multiple parts of speech ch1-03.l

%{
/*
* We expand upon the first example by adding recognition of some other
* parts of speech.
*/
%}
%%
[\t ]+ /* ignore whitespace */ ;
is |
am |
are |
were |
was |
be |
being |
been |
do |
does |
did |
will |
would |
should |
can |
could |
has |
have |
had |
go { printf("%s: is a verb\n", yytext); }
very |
simply |
gently |
quietly |
calmly |
angrily { printf("%s: is an adverb\n", yytext); }to |
from |
behind |
above |
below |
between
below { printf("%s: is a preposition\n", yytext); }
if |
then |
and |
but |
or { printf("%s: is a conjunction\n", yytext); }
their |
my |
your |
his |
her |
its { printf("%s: is a adjective\n", yytext); }
I |
you |
he |
she |
we |
they { printf("%s: is a pronoun\n", yytext); }
[a-zA-Z]+ {
printf("%s: don't recognize, might be a noun\n", yytext);
}
.|\n { ECHO;/* normal default anyway */ }
%%
main()
{
yylex();
}

符号表 Symbol Tables

我们的第二个示例其实并没有太大不同。与之前相比,我们列出了更多单词,理论上可以按需扩展更多词汇。然而,如果能在词法分析器运行时动态构建词汇表,就能在不修改和重新编译lex程序的情况下添加新词,这会更加便捷。
在我们的下一个例子中,我们正是这样做的,在程序运行时,会从输入文件中读取要声明的单词,这样能够允许在词法分析器运行时动态声明词性。声明行(即定义词性的行)以词性名称开头,后跟要声明的单词。

noun dog cat horse cow
verb chew eat lick

上面的单词表是一个简单的符号表,这是lex和yacc应用中常见的数据结构。例如,C编译器会将程序中使用的变量名、结构体名、标签、枚举标记等所有名称存储在其符号表中。每个名称都附带描述其属性的信息。在C编译器中,这些信息包括符号类型、作用域、变量类型等。而在我们当前的示例中,这些信息指的是词性。

添加符号表显著改变了词法分析器的结构。我们不再为每个单词设置独立的匹配模式,而是采用一个通用模式来匹配所有单词,并通过查询符号表来确定识别到的词性。此时,词性名称(如名词、动词等)成了“保留字”,因为它们用于引入声明行。我们仍需为每个保留字设置独立的词法模式。此外,还需添加符号表相关函数:本例中的 add_word() 用于将新词加入符号表,而 lookup_word() 则用于查询已录入的单词。

在程序代码中,我们声明了一个变量 state,用于追踪当前是处于查询模式(LOOKUP),还是处于声明模式——在后一种情况下,state 会记录我们正在声明的是哪种词性。每当我们遇到以词性名称开头的行时,就将状态设置为声明该类单词;每次遇到换行符 \n 时,则切换回正常的查询状态。

Example 1-3. Lexer with symbol table (part 1 of 3) ch1-04.l

%{
/*
* Word recognizer with a symbol table.
*/
enum {
LOOKUP =0, /* default - looking rather than defining. */
VERB,
ADJ,
ADV,
NOUN,
REP,
PRON,
CONJ
};
int state;
int add_word(int type, char *word);
int lookup_word(char *word);
%}

我们定义了一个枚举类型,以便在符号表中记录各个单词的类型,并用于声明状态变量 state。该枚举类型既用于 state 变量以追踪当前定义模式,也用于符号表中记录每个已定义单词的词性。同时,我们还声明了符号表操作函数。

Example 1-4 shows the rules section.


%%\n  { state = LOOKUP; } /*end of line, return to default state *//* whenever a line starts with a reserved part of speech name *//* start defining words of that type */^verb { state = VERB; }
^adj { state = ADJ; }
^adv { state = ADV; }
^noun { state = NOUN; }
^prep { state = PREP; }
^pron { state = PRON; }
^conj { state = CONJ; }[a-zA-Z]+ {/* a normal word, define it or look it up */if(state != LOOKUP){add_word(state, yytext);}else{switch(lookup_word(yytext)){case VERB: printf("%s: verb\n", yytext); break;case ADJ: printf("%s: adjective\n", yytext); break;case ADV: printf("%s: adverb\n", yytext); break;case NOUN: printf("%s: noun\n", yytext); break;case PREP: printf("%s: preposition\n", yytext); break;case PRON: printf("%s: pronoun\n", yytext); break;case CONJ: printf("%s: conjunction\n", yytext); break;default:printf("%s: don't recognize\n", yytext);break;}}
}.   /* ignore anything else */;
%%

对于声明单词,第一组规则将状态设置为对应要声明的词性。(模式开头的脱字符“^”使得该模式仅在输入行的开头匹配。)我们在每行开始时将状态重置为LOOKUP,以便在交互式添加新词后,能够测试词汇表是否正常工作。当模式“[a-zA-Z]+”匹配时,如果状态为LOOKUP,我们使用lookup_word()查找该单词,若找到则打印其类型;若处于其他任何状态,则使用add_word()定义该单词。

Example 1-5. Lexer with symbol table (part 3 of 3) ch1-04.l


main()
{
yylex();
}struct word 
{char *word_name;int word_type;struct word *next;
};struct word *word_list; // first element in word listextern void *malloc();int add_word(int type, char *word)
{struct word *wp;if(lookup_word(word) != LOOKUP){printf("!!! warning: word %s already defined \n", word);return 0;}/* word not there, allocate a new entry and link it on the list */wp = (struct word *) malloc(sizeof(struct word));wp->next = word_list;/* have to copy the word itself as well */wp->word_name = (char *) malloc(strlen(word)+1);strcpy(wp->word_name, word);wp->word_type = type;word_list = wp;return 1; /* it worked */
}int
lookup_word(char *word)
{struct word *wp = word_list;/* search down the list looking for the word */for(; wp; wp = wp->next) {if(strcmp(wp->word_name, word) == 0)return wp->word_type;}return LOOKUP; /* not found */
}

最后这两个函数创建并搜索一个单词链表。如果单词数量很多,这些函数的运行速度会很慢,因为每个单词都可能需要遍历整个链表。在生产环境中,我们会采用更快但更复杂的方案,很可能使用哈希表。不过,我们这个简单的示例虽然速度较慢,但确实完成了任务。

以下是我们使用最终示例进行的一个交互会话记录:

verb is am are was were be being been do
is
is: verb
noun dog cat horse cow
verb chew eat lick
verb run stand sleep
dog run
dog: noun
run: verb
chew eat sleep cow horse
chew: verb
eat: verb
sleep: verb
cow: noun
horse: noun
verb talk
talk
talk: verb

我们强烈建议您动手尝试这个示例,直到您确信自己已完全理解其工作原理。

使用Yacc解析语法

对于某些应用场景,我们已实现的简单单词识别功能可能已完全够用;而其他应用则需要识别特定的token符号序列并执行相应操作。传统上,这类操作的集合(set)的描述被称为语法。被称为语法。这对我们的示例来说尤其贴切。假设我们希望识别常见句型,以下是一些简单的句子类型列表:

noun verb.
noun verb noun.

至此,引入一种用于描述语法的标记法似乎十分便利。我们使用右箭头“→”表示一组特定的记号(token)可以被一个新的符号(symbol)所替换。例如:

subject → noun | pronoun

这表明新符号 subject 可以是名词或代词。我们并未改变底层符号的含义,而是从已定义的基本符号中构建了新的符号。再举一个例子,我们可以按如下方式定义 object

object → noun

虽然这严格来说并不符合英语语法,但我们现在可以定义一个句子:

sentence → subject verb object

实际上,我们可以扩展对句子的定义以适应更多样化的句子。然而,在此阶段,我们希望构建一个 yacc 语法,以便能够交互式地测试我们的想法。在引入 yacc 语法之前,我们必须修改词法分析器,使其返回对新解析器(parser)有用的值。

语法解析器和词法分析器的交互 Parser-Lexer Communication

当您将lex扫描器与yacc解析器结合使用时,解析器是更高层的函数。每当它需要从输入中获取一个记号时,就会调用词法分析器 yylex()。接着,词法分析器(lexer)扫描输入并识别记号。一旦找到语法解析器(parser)关注的记号,它便立即返回到解析器,并将该token记号的代码作为 yylex() 的返回值。

token记号也是symbol符号,但不是所有的symbol符号都是记号,只有从lexer词法分析器里返回的符号才被称作token记号。

并非所有记号都是解析器所关注的——例如,在大多数编程语言中,解析器不需要处理注释和空白符。对于这些被忽略的记号,词法分析器不会返回,以便继续识别下一个记号,而不会打扰解析器。

词法分析器与解析器必须就记号代码的定义达成一致。我们通过让 yacc 来定义这些代码解决此问题。我们语法中的记号是词性:名词代词动词副词形容词介词连词。Yacc 使用预处理器 #define 将它们每个都定义为一个小的整数。以下是它在本例中使用的定义:

# define NOUN 257
# define PRONOUN 258
# define VERB 259
# define ADVERB 260
# define ADJECTIVE 261
# define PREPOSITION 262
# define CONJUNCTION 263

输入流逻辑结束时总是返回记号代码 0。Yacc 并未为此定义一个符号,但如果您需要,可以自行定义。

Yacc 可以选择性地生成一个包含所有记号(token)定义的 C 头文件。在 UNIX 系统上该文件通常名为 y.tab.h,在 MS-DOS 系统上则可能为 ytab.hyytab.h。您需要将此文件包含到词法分析器中,并在词法分析器的行为代码(action code)中使用这些预处理器定义的符号。

词性词法分析器 The Parts of Speech Lexer

Example 1-6. Lexer to be called from the parser ch1-05.l


%{
/*
* We now build a lexical analyzer to be used by a higher-level parser.
*/
#include "y.tab.h" /* token codes from the parser */
#define LOOKUP 0 /* default - not a defined word type. */
int state;
%}%%\n { state = LOOKUP; }\.\n { state = LOOKUP;return 0; /* end of sentence */}
^verb { state = VERB; }
^adj { state = ADJECTIVE; }
^adv { state = ADVERB; }
^noun { state = NOUN; }
^prep { state = PREPOSITION; }
^pron { state = PRONOUN; }
^conj { state = CONJUNCTION; }
[a-zA-Z]+ {if(state != LOOKUP) {add_word(state, yytext);} else {switch(lookup_word(yytext)) {case VERB:return(VERB);case ADJECTIVE:return(ADJECTIVE);case ADVERB:return(ADVERB);case NOUN:return(NOUN);case PREPOSITION:return(PREPOSITION);case PRONOUN:return(PRONOUN);case CONJUNCTION:return(CONJUNCTION);default:printf("%s: don't recognize\n", yytext);/* don't return, just ignore it */}}}
. ;%%struct word 
{char *word_name;int word_type;struct word *next;
};struct word *word_list; // first element in word listextern void *malloc();int add_word(int type, char *word)
{struct word *wp;if(lookup_word(word) != LOOKUP){printf("!!! warning: word %s already defined \n", word);return 0;}/* word not there, allocate a new entry and link it on the list */wp = (struct word *) malloc(sizeof(struct word));wp->next = word_list;/* have to copy the word itself as well */wp->word_name = (char *) malloc(strlen(word)+1);strcpy(wp->word_name, word);wp->word_type = type;word_list = wp;return 1; /* it worked */
}int
lookup_word(char *word)
{struct word *wp = word_list;/* search down the list looking for the word */for(; wp; wp = wp->next) {if(strcmp(wp->word_name, word) == 0)return wp->word_type;}return LOOKUP; /* not found */
}

这里有几处重要的改动。我们调整了词法分析器中使用的词性名称,使其与解析器中的记号名称保持一致。同时,我们添加了返回语句,以便将识别到的单词对应的记号代码传递给解析器。对于那些向词法分析器定义新单词的记号,我们并未设置任何返回语句,因为解析器并不关心它们。

这些返回语句表明 yylex() 的行为类似于一个协程。每次解析器调用它时,它都会精准地从上次中断的位置继续处理。这使我们能够逐步检查和处理输入流。我们最初的程序无需利用这一特性,但随着我们将词法分析器用作大型程序的一部分,这一特性会变得更加有用。

我们添加了一条规则来标记句子的结束:

\.\n {   state = LOOKUP;return 0; /* end of sentence */}

规则中句点前的反斜杠对句点进行了转义,因此该规则匹配一个句点后接一个换行符。我们对词法分析器所做的另一处改动是省略了 main() 例程,因为它现在将由解析器提供。

Yacc语法解析器 A Yacc Parser

最后,示例 1-7 首次展示了我们初步构建的 yacc 语法。
Example 1-7. Simple yacc sentence parser ch1-05.y


%{
/*
* A lexer for the basic grammar to use for recognizing English sentences.
*/
#include <stdio.h>
%}%token NOUN PRONOUN VERB ADVERB ADJECTIVE PREPOSITION CONJUNCTION%%sentence: subject VERB object{ printf("Sentence is valid.\n"); };subject: NOUN| PRONOUN;object: NOUN;%%extern FILE *yyin;
main()
{do{yyparse(); }while (!feof(yyin));
}
yyerror(s)
char *s;
{
fprintf(stderr, "%s\n", s);
}

yacc 解析器的结构与 lex 词法分析器相似,这并非偶然。我们的第一部分,即定义部分,包含一个用%{%}括起来的字面代码块。这里我们用它来放置 C 语言注释(与 lex 一样,C 语言注释应放在 C 代码块内,至少在定义部分内如此)和一个单独的包含文件。

接着是定义我们预期从词法分析器获取的所有记号。在此示例中,它们对应着八种词性。对于 yacc 而言,记号的名称本身并不具有任何内在含义,但精心选择的名称能让阅读者明白其所代表的内容。尽管 yacc 允许使用任何有效的 C 语言标识符作为 yacc 符号,但通用惯例规定:记号token名称全部大写,而解析器中其他符号的名称则基本或完全使用小写。

第一个 %% 表示规则部分的开始。第二个 %% 表示规则的结束以及用户子程序部分的开始。最重要的子程序是 main(),它会反复调用 yyparse(),直到词法分析器的输入文件结束。例程 yyparse() 是由 yacc 生成的解析器,因此我们的主程序会反复尝试解析句子,直到输入耗尽。(词法分析器在每行末尾遇到句点时返回零值记号;这是向解析器发出信号,表明当前解析的输入已完成。)

规则部分 The Rules Section

规则部分将实际语法描述为一组产生式规则,或简称为规则(也有人称之为产生式)。每条规则由:运算符左侧的单个名称、右侧的符号列表和动作代码,以及表示规则结束的分号组成。默认情况下,第一条规则是最高层级的规则。也就是说,解析器会尝试寻找匹配此初始规则(或更常见的是,从初始规则推导出的规则)的记号序列。:右侧的表达式是一个包含零个或多个名称的列表。一个典型的简单规则在右侧只有一个符号,例如 object 规则被定义为一个 NOUN。规则左侧的符号随后可以像记号一样在其他规则中使用。我们以此为基础构建复杂的语法。

在我们的语法中,我们使用特殊字符“|”,它引入一条与前一规则左侧相同的规则。通常读作“或”,例如,在我们的语法中,主语可以是名词或代词。规则的行为部分由一个C代码块组成,以{开始,以}结束。解析器在规则匹配完成后立即执行对应的行为。 sentence 规则的行为Action会提示我们已成功解析一个句子。由于 sentence最上层符号,整个输入必须匹配一个 sentence。当词法分析器报告输入结束时,解析器会返回到其调用者(本例中是主程序)。后续对 yyparse() 的调用会重置状态并重新开始处理。如果我们的示例看到“subject VERB object”这样的输入记号序列,它会打印一条消息。但如果它看到“subject subject”或其他无效的记号序列会发生什么?解析器会调用我们定义在用户子程序部分中的 yyerror() 函数,然后识别特殊的 error 规则。您可以提供错误恢复代码,尝试使解析器回到能够继续解析的状态。如果错误恢复失败,或者像本例这样没有提供错误恢复代码,yyparse() 会在发现错误后返回给调用者。

第三个也是最后一个部分,即用户子程序部分,始于第二个 %% 之后。此部分可以包含任何 C 代码,并将被原样复制到生成的解析器中。在我们的示例中,我们向使用 yacc 生成的解析器提供了所需的最小函数集:main()yyerror()。主例程会持续调用解析器,直到到达 lex 输入文件 yyin 的末尾。另一个必需的例程 yylex() 由我们的词法分析器提供。

在本章最后的示例 1-8 中,我们扩展了之前的语法,以识别一组更丰富(尽管远非完整)的句子。我们建议您对这个示例进行更多实验——您将会看到,要以无歧义的方式描述英语是多么困难。


%{
#include <stdio.h>
%}%token NOUN PRONOUN VERB ADVERB ADJECTIVE PREPOSITION CONJUNCTION
%%sentence: simple_sentence { printf("Parsed a simple sentence.\n"); }| compound_sentence { printf("Parsed a compound sentence.\n"); };simple_sentence: subject verb object| subject verb object prep_phrase;compound_sentence: simple_sentence CONJUNCTION simple_sentence|   compound_sentence CONJUNCTION simple_sentence;subject: NOUN| PRONOUN| ADJECTIVE subject;verb: VERB| ADVERB VERB| verb VERB;object: NOUN| ADJECTIVE object;prep_phrase: PREPOSITION NOUN;%%extern FILE *yyin;main()
{do{yyparse();}while(!feof(yyin));
}yyerror(s)
char *s;
{fprintf(stderr, "%s\n", s);
}

我们扩展了句子规则,引入了小学英语课上的传统语法表述:一个句子可以是简单句,也可以是复合句——复合句包含两个或更多由并列连词连接的独立分句。我们当前的词法分析器并不区分并列连词(例如“and”、“but”、“or”)和从属连词(例如“if”)。

我们还将递归引入了此语法。递归(即一条规则直接或间接地引用自身)是描述语法的强大工具,我们编写的几乎每个 yacc 语法都会使用这项技术。在本例中,compound_sentenceverb 规则引入了递归。前者简单地规定:compound_sentence 是由连词连接的两个或多个简单句。第一种可能的匹配形式simple_sentence CONJUNCTION simple_sentence定义了“两个分句”的情况,而compound_sentence CONJUNCTION simple_sentence则定义了“超过两个分句”的情况。我们将在后续章节中更详细地讨论递归。

尽管我们的英语语法没有太大实用价值,但用 lex 识别单词、再用 yacc 找出单词间关系的技术,与我们将在后续章节的实际应用中所使用的方法基本相同。例如,在下面这条 C 语言语句中:

if( a == b ) break; else func(&a);

编译器会使用 lex 来识别记号 if(a== 等,然后使用 yacc 来确定 a == bif 语句的表达式部分,break 语句是其“真”分支,而函数调用则是其“假”分支。

运行Lex和Yacc Running Lex and Yacc

最后,我们描述一下如何在我们的系统上构建这些工具。我们将各个词法分析器文件命名为 ch1-N.l,其中 N 对应特定的 lex 规范示例。同样,我们将解析器文件命名为 ch1-M.y,其中 M 也是示例的编号。接着,在 UNIX 系统中,我们按以下步骤生成输出:

% lex ch1-n.l
% yacc -d ch1-m.y
% cc -c lex.yy.c y.tab.c
% cc -o example-m.n lex.yy.o y.tab.o -ll

第一行命令对 lex 规范运行 lex,并生成文件 lex.yy.c,其中包含词法分析器的 C 代码。第二行使用 yacc 同时生成 y.tab.cy.tab.h(后者是通过 -d 开关创建的记号定义文件)。第三行分别编译这两个 C 文件。最后一行将它们链接在一起,并使用了 lex 库 libl.a 中的例程(该库在大多数 UNIX 系统中通常位于 /usr/lib/libl.a)。如果您使用的不是 AT&T lex 和 yacc,而是其他实现,可能只需替换命令名称,其他步骤基本不变。(特别是,Berkeley yacc 和 flex 只需将 lex 和 yacc 命令改为 byaccflex,并移除 -ll 链接器标志即可。)然而,不同实现间存在太多差异,我们无法向读者保证在所有情况下都如此。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/1175107.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

Java计算机毕设之基于springboot的宠物医院就医预约管理系统的设计与实现(完整前后端代码+说明文档+LW,调试定制等)

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

Day21-20260117

本文介绍了Java中命令行参数传递和可变参数的使用方法,以及递归的基本概念。主要内容包括:1) 通过main函数接收命令行参数并输出;2) JDK1.5引入的可变参数特性,使用...语法实现不定参数传递;3) 递归的基本原理,包…

计算机Java毕设实战-基于springboot+vue的宠物医院管理系统的设计与实现【完整源码+LW+部署说明+演示视频,全bao一条龙等】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

Java毕设选题推荐:基于Java+springboot的宠物医院管理系统的设计与实现【附源码、mysql、文档、调试+代码讲解+全bao等】

博主介绍&#xff1a;✌️码农一枚 &#xff0c;专注于大学生项目实战开发、讲解和毕业&#x1f6a2;文撰写修改等。全栈领域优质创作者&#xff0c;博客之星、掘金/华为云/阿里云/InfoQ等平台优质作者、专注于Java、小程序技术领域和毕业项目实战 ✌️技术范围&#xff1a;&am…

开源上门预约系统源码,如何实现智能排班与时间冲突校验?

在上门服务类系统中&#xff0c;智能排班和时间冲突校验几乎决定了整个系统能否稳定运行。 一旦排班出错&#xff0c;轻则客户体验下降&#xff0c;重则直接造成投诉和订单损失。 本文将结合开源上门预约系统源码的常见设计&#xff0c;拆解排班与冲突校验的实现思路&#xff0…

开源上门预约系统源码,如何实现智能排班与时间冲突校验?

在上门服务类系统中&#xff0c;智能排班和时间冲突校验几乎决定了整个系统能否稳定运行。 一旦排班出错&#xff0c;轻则客户体验下降&#xff0c;重则直接造成投诉和订单损失。 本文将结合开源上门预约系统源码的常见设计&#xff0c;拆解排班与冲突校验的实现思路&#xff0…

深度测评!继续教育必看的9款AI论文写作软件TOP9推荐

深度测评&#xff01;继续教育必看的9款AI论文写作软件TOP9推荐 2026年继续教育AI论文写作工具测评&#xff1a;为何需要这份榜单&#xff1f; 随着人工智能技术的不断进步&#xff0c;AI论文写作工具在学术领域的应用越来越广泛。对于继续教育群体而言&#xff0c;如何高效完成…

Flutter for OpenHarmony 电子合同签署App实战 - 编辑资料实现

编辑资料功能是电子合同应用的重要组成部分。这个功能提供了完整的用户界面、数据管理和业务逻辑。通过编辑资料功能&#xff0c;用户可以更加高效地完成相关操作。在这篇文章中&#xff0c;我们将详细讲解如何实现一个功能完整、用户友好的编辑资料功能。通过学习本文&#xf…

QT跨平台一次编写,处处编译

在当今软件开发领域&#xff0c;“跨平台”已从一个技术理想转变为实际需求。当我们谈论跨平台框架时&#xff0c;QT无疑是其中最耀眼的名字之一。但QT究竟如何实现“一次编写&#xff0c;到处运行”的承诺&#xff1f;它的跨平台本质是什么&#xff1f; 抽象的艺术&#xff1a…

why Internet is bad?

if youre loser with a really short name, of course lacks a Chinese parent. the Internet will transform your loneliness as Hilter but the most shameless form.

使用WSL(Windows Subsystem for Linux) - 何苦

使用WSL(Windows Subsystem for Linux)适用场景 无需重启切换系统,直接在Windows中运行Linux命令行或图形界面,适合开发和学习。 优势 资源占用低,与Windows文件互通,支持原生Linux工具链。 操作步骤启用WSL功能…

博客与短视频谁更能成就你的个人品牌?

人物&#xff1a; 老派博客&#xff08;博哥&#xff09;——文字爱好者&#xff0c;崇尚深度与持久 新锐短视频&#xff08;抖妹&#xff09;——视觉达人&#xff0c;追求传播与互动 场景&#xff1a; 虚拟的“品牌咖啡馆”内&#xff0c;两人隔着桌子面对面坐着。空气中弥漫…

禁止windows11自动更新不反弹,win11永久关闭自动更新,win11怎么关闭系统自动更新

有信无证者&#xff0c;虽不落恶果&#xff0c;却住因住果&#xff0c;住念住心&#xff0c;如是生灭。不得涅槃。 【实测有效方法】 禁止windows11自动更新不反弹, win11永久关闭自动更新&#xff0c; win11怎么关闭系统自动更新 提供6种方法&#xff0c;需要直接简单方便…

2026 年机场广告投放公司综合实力排行榜单及选择建议指南:2026年机场广告投放公司如何选?哪家好?哪家靠谱?选哪家? - Top品牌推荐

一、机场广告投放市场概述 机场广告作为高端户外媒体的重要组成部分,凭借其覆盖高净值人群、强制性观看、品牌形象提升等优势,成为众多企业品牌推广的重要选择。机场媒体接触的旅客群体具有高收入、高学历、高消费能…

6种方法教你永久关闭win11系统自动更新【保姆级教程】,win11关闭自动更新的详细方法步骤

Win11系统自动更新怎么关闭&#xff1f;最近有不少小伙伴们想要彻底对Win11系统的自动更新进行关闭&#xff0c;但不知道应该怎么去操作。那我们在碰到这个问题要怎么办呢&#xff1f;还不清楚的小伙伴们可以不用担心&#xff0c;下边介绍几种常用的禁止win11自动更新的详细方法…

modelscope 上PaddleOCR-VL 部署(2026年1月17日亲测可用)

PaddleOCR-VL 部署创建时间: 2026-01-16 环境: ModelScope PAI-DSW 免费实例环境配置组件版本系统Ubuntu 22.04CUDA12.4.0Python3.11.11PyTorch2.9.1cuDNN1.33.0CPU8核内存32GB显存24GB剩余额度13小时30分钟 虚拟环境 wget http://qiniu.dywlkj.com/uv_for_linux_x86/install.s…

QtOpenGL多线程渲染方案深度解析

QtOpenGL多线程渲染方案深度解析1. 引言&#xff1a;为什么需要多线程渲染&#xff1f;2. QtOpenGL多线程架构设计2.1 基本线程模型2.2 关键组件3. 实现细节与性能优化3.1 线程间同步机制3.2 性能关键点4. 实战案例&#xff1a;3D场景编辑器4.1 架构设计4.2 性能对比5. 常见问题…

15.ACS725 电流检测

分享一颗电流传感器IC芯片&#xff0c;ACS725芯片&#xff0c;这颗芯片是汽车级的&#xff0c;工作温度在-40到150度&#xff0c;很多电流采集的场合可以使用。ACS725提供了一种小封装&#xff0c;低成本的表贴封装&#xff0c;而且目前在国内也有可以替换的一些IC出现&#xf…

千万会员,亿级交易:当CRM系统不堪重负,头部药企如何通过数据库升级实现“实时精准营销”?

千万会员,亿级交易:当CRM系统不堪重负,头部药企如何通过数据库升级实现“实时精准营销”?作者:张红霞,青岛雨诺网络信息股份有限公司新零售产品部总监 综述 当前,医药零售企业已不再满足于“卖药”,而是致力于…

YOLO26 改进 - 注意力机制 | IIA信息整合注意力(Information Integration Attention ):精准保留空间位置信息,平衡精度与计算成本 | TGRS2025

前言 本文提出信息整合注意力&#xff08;IIA&#xff09;机制&#xff0c;并将其集成到YOLO26中用于遥感图像语义分割。传统CNN难捕捉全局信息&#xff0c;Transformer计算复杂&#xff0c;现有基于Mamba的方法未充分考虑局部信息。IIA利用图像特征空间位置不变性&#xff0c…