什么,你要写AC自动机?什么,你要学编译原理?老哥你在逗我吗?我写解释器就是为了休闲娱乐,自己乱搞,搞得那么专业干什么?
曾经写过一个小巧的“解释器”,解释的是一门我自己YY出来的垃圾语言,虽说功能奇弱,不过从语法上讲也算得上要if有if,要while有while,足够用来计算一些简单的小学数学题,比如,判断个闰年、打印个一万以内的质数表之类的(欧拉筛?想得美,我没有数组…)。某C#资深大神看了我上一个版本解释器的C++源代码之后,深感代码丑陋,逻辑性极差,不堪卒读(请同学们注意,成语“不堪卒读”和“不忍卒读”在含义上有着微妙地差异),缺少解释器所应具备的大多数组成成分。为此,我打算重新写一个…(我还真是个“倔强(youqu)”的人啊。)
好了,作为一个热爱信竞(baoli)的孩子,我们一同踏上了一条静态查错的不归路。警告:下文内容纯属胡扯,如果有人因为信了我的话,导致了不良后果(比如挂科之类的),我绝不负责。解释的语言仍是我自己YY出来的,但与之前的相比有所不同。
把混乱的文件(用暴力的方法),拆成单词的组合,从而提升文件的有序性。我打算浪费一点硬盘空间和处理时间,每一步处理时,都生成出一个处理中间文件,用以分析程序错误。下一步的处理,将会利用到上一步生成的中间文件。
在实际操作之前,我们需要做一些必要的定义。
由于我打算模糊化对汉字等特殊符号的处理,所以,在这里我们姑且认为一个字节(Byte)中所储存的信息,就代表一个字符。为了描述方便我们将字符分为以下几类:
编号 | 分类 | ASCII码范围 | 集合名称 |
---|---|---|---|
1 | 特殊符 | [ − 128 , − 2 ] [-128,-2] [−128,−2] | SpecialChar |
2 | 文件尾 | { − 1 } \{-1\} {−1} | EndOfFile (EOF) |
3 | 控制符 | [ 0 , 8 ] ∪ { 11 , 12 , 127 } ∪ [ 14 , 31 ] [0,8]\cup\{11, 12, 127\}\cup[14,31] [0,8]∪{11,12,127}∪[14,31] | Controller |
4 | 空白符 | { 32 , 9 , 10 , 13 } \{32, 9, 10, 13\} {32,9,10,13} | SpaceChar |
5 | 算符 | [ 33 , 47 ] ∪ [ 58 , 64 ] ∪ [ 91 , 94 ] ∪ { 96 } ∪ [ 123 , 126 ] [33,47]\cup[58,64]\cup[91,94]\cup\{96\}\cup[123,126] [33,47]∪[58,64]∪[91,94]∪{96}∪[123,126] | Operator |
6 | 数字 | [ 48 , 57 ] [48,57] [48,57] | NumeralChar |
7 | 大写字母 | [ 65 , 90 ] [65,90] [65,90] | CapitalChar |
8 | 小写字母 | [ 97 , 122 ] [97,122] [97,122] | LowerChar |
9 | 下划线 | { 95 } \{95\} {95} | UnderLine |
以上的九个集合,覆盖了一个字节能表示的全部符号,写一段简单而无聊的代码实现字符分类。
const int SpecialChar = 1, /// 定义了一些与字符分类有关的常量
EndOfFile = 2,
Controller = 3,
SpaceChar = 4,
Operator = 5,
NumeralChar = 6,
CapitalChar = 7,
LowerChar = 8,
UnderLine = 9;
#define Rin(A, B, C) (((A)<=(B))&&((B)<=(C))) /// 判断 B是否在闭区间 [A, C]内
int GetCharType(char c) { /// 实现字符分类
if(Rin(-128, c, -2)) return SpecialChar; /// 特殊字符
else
if(c == -1) return EndOfFile; /// 文件尾
else
if(Rin(0, c, 8) || Rin(14, c, 31) || Rin(11, c, 12) || c == 127) return Controller; /// 控制符
else
if(c == 32 || c == 9 || c == 10 || c == 13) return SpaceChar; /// 空白符
else
if(Rin(33, c, 47) || Rin(58, c, 64) || Rin(91, c, 94) || c == 96 || Rin(123, c, 126)) return Operator; /// 算符
else
if(Rin(48, c, 57)) return NumeralChar; /// 数字
else
if(Rin(65, c, 90)) return CapitalChar; /// 大写
else
if(Rin(97, c, 122)) return LowerChar; /// 小写
else
if(c == 95) return UnderLine; /// 下划线
else
return -1; /// 理论上不会出现找不到字符分类的情况
}
验证这个程序的正确性,尽管这么智障的程序,我们也要严谨。(验证程序略)
由于我懒得学习正则表达式,组词规则将用自然语言或代码描述,尽管自然语言描述肯能并不严谨。
ASC(46)
)外)或文件结尾。ASC(46)
),而不能包含其他字符,且小数点在字符串中出现且仅出现一次。ASC(46)
)外)或文件结尾。ASC(46)
)外)或文件结尾。0x
为前缀的、长度大于等于3的字符串,除前两个字符外,这个字符串剩余的部分只能包含以下的22种字符:数字,小写字母a~ASC(46)
)外)或文件结尾。ASC(34)
),其余字符可以是除双引号、回车符(即ASC(13)
)、换行符(即ASC(10)
)和文件尾(即ASC(-1)
)外的任何字符。( ) [ ] . { } ; + - * / % = > >= < <= ^ == <>
。(确实是21个,如果你能数明白,说明你能理解我的意思。)(同时注意区分“算符”和“运算符”的概念。)/*
”是这个字符串的一个前缀,“*/
”是这个字符串的一个后缀,除了字符串的最后两个字符外,这个字符串中不含有子串“*/
”。ASC(32)
)和Tab(ASC(9)
)两种字符。(注意区分“空白”和“空白符”的涵义。)\13\10
”,“\13
”,“\10
”。(p.s.:“\x
(x为整数)”表示ASCII码为x的字符。)(注意区分“换行”与“换行符”的涵义。)ASC(10)
),对其后的第一个字符没有要求。若换行的最后一个字符为回车(ASC(13)
),则要求换行后的第一个字符不能为换行符(ASC(10)
)。ASC(-1)
)。不难发现,以上词法分析过程可以通过手动构造自动机轻松实现(我说我不想写AC自动机,不代表我不手动构造自动机)。
先给出手稿,具体信息留坑待补:
这个词法分析自动机的正确性、严谨性是有待考证的,因此先不给出自动机的数据。(tips:有的else边可能没写上。)
写一个字节的伪缓冲区即可。
char localTmp; /// 伪缓冲区
bool localTmpHasValue = 0; /// 表明伪缓冲区中是否有字符
char GetChar(FILE* fpin) { /// 从文件中读取一个字符(考虑伪缓冲区)
if(localTmpHasValue) { /// 伪缓冲区非空
localTmpHasValue = false; /// 清空伪缓冲区
return localTmp; /// 返回伪缓冲区的值
}else {
localTmp = fgetc(fpin);
return localTmp; /// 在文件中重新读取一个字节
}
}
void BackChar() { /// 将最后一次读出的字符退还给缓冲区
if(localTmpHasValue) {
fprintf(stderr, "缓冲区- 错误:不能连续向缓冲区退还两个字符 ...");
while(1);
}else {
localTmpHasValue = 1; /// 退还最后一次读出的字符
}
}
我们可以用一个三元组来描述得到的词法单元:(组词规则编号,串长度,原内容字符串)。实际上,在词法分析过程中,我们可以丢弃注释和空白。但我们不能丢弃换行,因为换行决定着当前光标在文件中所处的行数,行数在错误信息的输出中有重要意义。
对自动机的描述可以分为两部分,对点的描述,对边的描述。
编号 | 集合名称 | 集合内容 |
---|---|---|
10 | CLU |
C a p i t a l C h a r ∪ L o w e r C h a r ∪ U n d e r L i n e CapitalChar \cup LowerChar \cup UnderLine CapitalChar∪LowerChar∪UnderLine |
11 | SP |
{ 32 , 9 } \{32,9\} {32,9} |
12 | N |
与 NumeralChar 完全相同 |
13 | NZ |
[ 49 , 57 ] [49,57] [49,57] (即非零数字) |
14 | SOE |
S p a c e C h a r ∪ O p e r a t o r ∪ E n d O f F i l e − { 46 } SpaceChar\cup Operator \cup EndOfFile - \{46\} SpaceChar∪Operator∪EndOfFile−{46} |
15 | dot |
{ 46 } \{46\} {46} |
16 | NAF |
[ 48 , 57 ] ∪ [ 97 , 102 ] ∪ [ 65 , 70 ] [48,57]\cup [97,102] \cup [65,70] [48,57]∪[97,102]∪[65,70] |
17 | OUS |
{ 40 , 41 , 91 , 93 , 46 , 123 , 125 , 59 , 43 , 45 , 42 , 94 , 37 } \{40, 41, 91, 93, 46, 123, 125, 59, 43, 45, 42, 94, 37\} {40,41,91,93,46,123,125,59,43,45,42,94,37} |
简单解释一下这样定义的原因:
集合名称 | 解释 |
---|---|
CLU |
标识符组词规则中,第一个字符必须从属于这个集合。 |
SP |
空白组词规则中,所有字符必须从属于这一集合。 |
N |
十进制整数,十进制实数组词规则中经常出现这一集合中的字符。 |
NZ |
十六进制数以“0x ”为前缀,需要特殊考虑字符“0 ”。 |
SOE |
很多组词规则,要求组词结束后的第一个字符属于这一集合。(注:这一集合并不包含dot (ASC(46) )) |
dot |
这个集合没必要定义,我纯属闲的。其中只含有一个字符——点(也就是英文句号)。 |
NAF |
数字、a~f、A~F,这是十六进制数中常出现的元素集合。 |
OUS |
Operators which are Used Separately。在运算符组词规则中,只能独自构成运算符的字符。它们是 ( ) [ ] . { } ; + - * ^ % 。 |
我们用符号else
表示一个特殊的集合,对于某一个结点,这个结点最多只有一条标有else
的出边,表示如果这个结点的其他出边都不能与当前输入的字符匹配,那么,它能够与标有else
的这条出边匹配。
(有几种算符可以与其他算符构成运算符,比如“>
”、“=
”和“<
”,它们可以构成“>=
”、“<=
”、“==
”和“<>
”四种长度为两个字节的运算符。同时,它们自身也可以单独构成运算符,因此需对它们逐个单独考虑。)
int CheckCharType(int id, char c) { /// 判断字符是否属于某一集合
if(Rin(1, id, 9)) {
return GetCharType(c) == id; /// 如果被判断集合为 9个基本字符集合
}else {
int charType = GetCharType(c); /// 得到基本集合信息
#define cT charType
if(id == 10) return cT == CapitalChar || cT == LowerChar || cT == UnderLine; /// _CLU
else
if(id == 11) return c == 32 || c == 9; /// _SP
else
if(id == 12) return cT == NumeralChar; /// _N
else
if(id == 13) return Rin(49, c, 57); /// _NZ
else
if(id == 14) return (cT == SpaceChar || cT == Operator || cT == EndOfFile) && c != 46; /// _SOE
else
if(id == 15) return c == 46; /// _dot
else
if(id == 16) return Rin(48, c, 57) || Rin(97, c, 102) || Rin(65, c, 70); /// _NAF
else
if(id == 17) return c==40 ||c==41||c==91||c==93||c==46||c==123||
c==125||c==59||c==43||c==45||c==42||c==94 ||c==37; /// _OUS
else {
return -1; /// error 可能是集合 id非法
}
#undef cT
}
}
根据以上的内容,在此制定用文本文件描述自动机信息的格式:
"CLU","SP","N","NZ","SOE","dot","NAF","OUS","else"
这九个字符串之一(不含双引号和逗号),则认为这条边能够匹配,以字符串 i i i为名字的字符集合中的任意一个字符。这种字典树在整个解释器的实现过程中有着很多的用处,在这里,我们用它来实现集合名字符串与集合编号的转换。
template<class _T, int MaxNodeCnt>
class Tire { /// 字典树模板类 字符串到 _T类型的映射
/// 采用兄弟节点表示法 (#0点为根)
int sSon[MaxNodeCnt]; /// 最后一个加入的儿子
int sBro[MaxNodeCnt]; /// 年龄最小的兄弟
char fChr[MaxNodeCnt]; /// 父亲边能够识别的字符
bool wVal[MaxNodeCnt]; /// 是否带有值
_T tVal[MaxNodeCnt]; /// 所带有的值
int nCnt; /// 当前节点数目
void Clear() { /// 清空字典树
nCnt = 0; /// 当前只有根节点
sSon[0] = -1;
sBro[0] = -1; /// 根节点没有兄弟和儿子
wVal[0] = 0; /// 不带有值
}
int NewNode() { /// 新申请一个节点
if(nCnt == MaxNodeCnt - 1) { /// 字典树已满
fprintf(stderr, "字典树- 错误:字典树已满");
while(1);
}
nCnt ++; /// 新申请一个结点
sSon[nCnt] = -1;
sBro[nCnt] = -1;
wVal[nCnt] = 0; /// 不带有值
return nCnt;
}
void AddSon(int f, int s, char c) { /// 给 f添加一个儿子 s
sBro[s] = sSon[f];
sSon[f] = s; /// 承认父子关系
fChr[s] = c; /// 父亲边能识别的字符
}
public:
Tire() {
Clear();
}
void ClearTire(){ /// 清空字典树
Clear();
}
bool Exist(const char* str) { /// 判断字符串存在于定义域中
int rtn = 0; /// 当前根节点
for(int i = 0; str[i] != 0; i ++) { /// 扫描字符串 str
char cn = str[i]; /// 当前字符
bool suc = false; /// 记录是否匹配成功
for(int v = sSon[rtn]; ~v; v = sBro[v]) { /// 依次访问所有儿子
if(fChr[v] == cn) { /// 匹配成功
rtn = v; /// 进入子树
suc = true; /// 匹配成功
break;
}
}
if(!suc) {
return false;
}
}
return wVal[rtn];
}
_T GetValue(const char* str) { /// 判断字符串存在于定义域中
int rtn = 0; /// 当前根节点
for(int i = 0; str[i] != 0; i ++) { /// 扫描字符串 str
char cn = str[i]; /// 当前字符
bool suc = false; /// 记录是否匹配成功
for(int v = sSon[rtn]; ~v; v = sBro[v]) { /// 依次访问所有儿子
if(fChr[v] == cn) { /// 匹配成功
rtn = v; /// 进入子树
suc = true; /// 匹配成功
break;
}
}
if(!suc) {
return tVal[0];
}
}
return tVal[rtn];
}
void SetValue(const char* str, _T newValue) {
int rtn = 0; /// 当前根节点
for(int i = 0; str[i] != 0; i ++) { /// 扫描字符串 str
char cn = str[i]; /// 当前字符
bool suc = false; /// 记录是否匹配成功
for(int v = sSon[rtn]; ~v; v = sBro[v]) { /// 依次访问所有儿子
if(fChr[v] == cn) { /// 匹配成功
rtn = v; /// 进入子树
suc = true; /// 匹配成功
break;
}
}
if(!suc) { /// 未成功匹配
int s = NewNode(); /// 新申请结点
AddSon(rtn, s, cn); /// 建立对应结点
rtn = s;
}
}
wVal[rtn] = true;
tVal[rtn] = newValue; /// 设立新值
}
void Erase(const char* str) { /// 删除字符串
int rtn = 0; /// 当前根节点
for(int i = 0; str[i] != 0; i ++) { /// 扫描字符串 str
char cn = str[i]; /// 当前字符
bool suc = false; /// 记录是否匹配成功
for(int v = sSon[rtn]; ~v; v = sBro[v]) { /// 依次访问所有儿子
if(fChr[v] == cn) { /// 匹配成功
rtn = v; /// 进入子树
suc = true; /// 匹配成功
break;
}
}
if(!suc) { /// 找不到对应的字符串
return ;
}
}
wVal[rtn] = false;
}
};
手造数据测试字典树的正确性:
Tire<int, 100> test; ///验证字典树的正确性
int main() {
while(1) {
int opt; char s[10]; int val;
printf("--> ");
scanf("%d", &opt);
if(opt == 1) { /// 修改/插入一个值
scanf("%s%d", s, &val);
test.SetValue(s, val);
}else if(opt == -1) { /// 删除一个值
scanf("%s", s);
test.Erase(s);
}else if(opt == 0) { /// 查询一个值
scanf("%s", s);
if(!test.Exist(s)) {
printf("Not Exist\n");
}else {
printf(" = %d. \n", test.GetValue(s));
}
}else {
printf("opt error\n");
}
}
return 0;
}
构造用于识别字符集合的字典树:
Tire<int, 30> ClassId; /// 字符集合名称编号转换
void InitClassId() { /// 初始化这个集合
ClassId.SetValue("CLU", 10);
ClassId.SetValue("SP" , 11);
ClassId.SetValue("N" , 12);
ClassId.SetValue("NZ" , 13);
ClassId.SetValue("SOE", 14);
ClassId.SetValue("dot", 15);
ClassId.SetValue("NAF", 16);
ClassId.SetValue("OUS", 17);
ClassId.SetValue("else", -1);
}
不像Tire模板类在很多地方都能派上用场,自动机(有限状态机)模板类只在词法分析过程中起重要作用。
template<int MaxNodeCnt>
class autoMachine { /// 自动机模板类
int N, M; /// 自动机的点数、边数
int typ[MaxNodeCnt]; /// 记录结点是哪一种结点
int nxt[MaxNodeCnt][256]; /// 记录邻接矩阵
char msg[MaxNodeCnt][256]; /// 记录错误信息
int mtd[MaxNodeCnt]; /// 接收结点 的 组词规则
int els[MaxNodeCnt]; /// 记录 els边的指向
int now; /// 现在所在的结点
void Clear() {
memset(nxt, 0xff, sizeof(nxt)); /// nxt 初始化为 -1
memset(typ, 0xff, sizeof(typ)); /// typ 初始化为 -1
memset(els, 0xff, sizeof(els)); /// els 初始化为 -1
}
void Link(int f, int t, unsigned char c, int eId) { /// 连接两点(单边)
if(nxt[f][c] == -1) {
nxt[f][c] = t;
}else {
fprintf(stderr, "自动机- 错误:边的重复连接, eId = %d\n", eId);
while(1);
}
}
void AddEdge(int f, int t, const char* val, int eId) { /// 从 f向 t连边(可能是集合边)
if(Rin('0', val[0], '9') || val[0] == '-') { /// val中存着数字
int c;
sscanf(val, "%d", &c); /// 读入这个数字
Link(f, t, c, eId);
}else { /// val中存放着集合名
if(!ClassId.Exist(val)) { /// 集合名不存在
fprintf(stderr, "自动机- 错误:集合名(%s)不存在 eId = %d\n", val, eId);
}else {
int id = ClassId.GetValue(val); ///得到集合 id
if(id == -1) { /// else 边
for(int c = 0; c <= 255; c ++) { /// 枚举出边
if(nxt[f][c] == -1) {
Link(f, t, c, eId); /// 连接所有未连之边
}
}
}else {
for(int c = 0; c <= 255; c ++) {
if(CheckCharType(id, c)) { /// 连接集合规定的所有边
Link(f, t, c, eId);
}
}
}
}
}
}
bool CheckNodeFull(int id) { /// 检测某个点是否有全部出边
for(int i = 0; i <= 255; i ++) {
if(nxt[id][i] == -1) return false; /// 有空出边
}
return true;
}
bool StrSame(const char* s1, const char* s2) {
for(int i = 0; ; i ++) {
if(s1[i] != s2[i]) return false;
if(s1[i] == 0) break;
}
return true;
}
public:
int line; /// 文本中所处的行数
void ClearAutoMachine() {
Clear();
line = 0;
}
AutoMachine() {
ClearAutoMachine();
}
void ReStart(int startNode = 1) { /// 自动机的起始状态
now = startNode;
}
bool CheckFull() { /// 检测是否每个普通结点都有全部出边
bool flag = true;
for(int i = 1; i <= N; i ++) {
if(!typ[i] && !CheckNodeFull(i)) { /// 检测每个普通节点是否有全部出边
flag = false;
fprintf(stderr, "自动机- 警告:普通结点 i = %d 出边不完备\n", i);
}
}
if(!flag) {
fprintf(stderr, "自动机- 错误:自动机出边不完备");
while(1);
}
return flag;
}
void InputFromFile(const char* filepath) { /// 从文件读入自动机
FILE* fpin = fopen(filepath, "r");
if(fpin == NULL) {
fprintf(stderr, "自动机- 错误:指定文件(%s)不可读入", filepath);
while(1);
}
fscanf(fpin, "%d%d", &N, &M); /// 输入自动机的点数和边数
for(int i = 1; i <= N; i ++) { /// 输入点的信息
int v;
fscanf(fpin, "%d", &v); /// 输入当前被描述点的编号
if(typ[v] != -1) {
fprintf(stderr, "自动机- 错误:点信息的重复描述(点信息- 第 %d 行) v = %d", i, v);
while(1);
}
int t;
fscanf(fpin, "%d", &t);
if(!Rin(0, t, 3)) {
fprintf(stderr, "自动机- 错误:结点类型描述不合法(点信息- 第 %d 行) v = %d", i, v);
while(1);
}
typ[v] = t;
if(t == 1) { /// 报错结点
fscanf(fpin, "%s", msg[v]); /// 输入报错信息
}else if(t != 0){
fscanf(fpin, "%d", &mtd[v]); /// 输入组词规则编号
}
}
for(int i = 1; i <= M; i ++) { /// 输入边信息
int e;
fscanf(fpin, "%d", &e); /// 输入边的编号 (没用)
int p, q;
fscanf(fpin, "%d%d", &p, &q); ///输入所连接的点
char I[256];
fscanf(fpin, "%s", I); /// 输入集合
if(StrSame(I, "else")) { /// else边 必须最后插入
if(els[p] != -1) {
fprintf(stderr, "自动机- 错误:点 p = %d, 被插入两条else边, eId = %d", p, e);
while(1);
}else {
els[p] = q;
}
}else AddEdge(p, q, I, e);
}
for(int i = 1; i <= N; i ++) { /// 插入所有else边
if(els[i] != -1)
AddEdge(i, els[i], "else", -1); /// eId散轶
}
}
int PushForward(unsigned char c, int& mTD) { /// 自动机状态 向前进
now = nxt[now][c]; /// 向前进
if(now == -1) {
fprintf(stderr, "自动机- 错误:进入虚空节点"); /// 可能是接受字符串后忘记 ReStart导致
while(1);
}
if(typ[now] == 1) { /// 报错
fprintf(stderr, "自动机- 信息:报错结点 now = %d:%s, 行数 %d\n", now, msg[now], line);
while(1);
}else if(typ[now]) { /// mTD 接收组词规则
mTD = mtd[now];
}
return typ[now]; /// 返回当前结点类型
}
};
给出一个自动机文件:
44 69
1 0
2 0
3 1 未封闭的双引号
4 1 正文中的非法字符
5 2 6
6 0
7 0
8 0
9 2 8
10 3 7
11 1 未封闭的多行注释
12 0
13 3 1
14 1 标识符中的非法字符
15 0
16 0
17 1 数值中的非法字符
18 3 5
19 0
20 0
21 0
22 0
23 3 4
24 3 2
25 1 数值中的非法字符
26 3 3
27 1 科学计数法中的非法字符
28 2 7
29 0
30 2 7
31 0
32 2 7
33 3 7
34 3 7
35 0
36 0
37 3 10
38 2 10
39 2 7
40 3 9
41 0
42 2 7
43 3 7
44 2 11
1 1 4 else
2 1 2 34
3 2 2 else
4 2 3 -1
5 2 3 13
6 2 3 10
7 2 5 34
8 1 6 47
9 6 7 42
10 7 7 else
11 7 8 42
12 8 9 47
13 1 12 CLU
14 6 10 else
15 8 7 else
16 8 11 -1
17 7 11 -1
18 1 15 48
19 12 12 CLU
20 12 12 N
21 12 13 dot
22 12 13 SOE
23 12 14 else
24 15 17 else
25 15 16 120
26 16 16 NAF
27 16 17 else
28 16 18 SOE
29 1 19 NZ
30 15 19 N
31 15 24 SOE
32 19 24 SOE
33 19 19 N
34 19 20 dot
35 19 25 else
36 20 20 N
37 20 25 else
38 20 26 SOE
39 20 21 69
40 20 21 101
41 21 27 else
42 21 22 N
43 21 22 45
44 21 22 43
45 22 22 N
46 22 23 SOE
47 22 27 else
48 1 28 OUS
49 1 29 62
50 29 30 61
51 29 33 else
52 1 31 61
53 31 32 61
54 31 34 else
55 1 41 60
56 41 39 61
57 41 42 62
58 41 43 else
59 1 44 -1
60 1 35 SP
61 1 36 13
62 1 38 10
63 35 35 SP
64 35 36 13
65 36 37 else
66 36 38 10
67 35 40 else
68 35 38 10
69 15 20 dot
给出这个自动机文件对应的手稿:
(留坑待补)
给出测试时用的代码:
autoMachine<50> LexicalAnalysis; /// 词法分析机
char tmpStr[1024]; /// 词法分析缓冲区
int charCnt; /// 缓冲区中字符数量
void LAtest(const char* filepath) { /// 词法分析测试
FILE* fpin = fopen(filepath, "r");
if(fpin == NULL) {
fprintf(stderr, "词法分析测试- 错误:指定分析文件(%s)不可读", filepath);
while(1);
}
localTmpHasValue = 0; /// 清空缓冲区
#define LA LexicalAnalysis
LA.ReStart(1); /// 开始读入
int mtd = 0; /// 最后一次接收时采用的组词规则
bool isEnd = 0; /// 记录是否读到文件尾
do {
char cn = GetChar(fpin);
tmpStr[charCnt ++] = cn; /// 读入一个字符
int stat = LA.PushForward(cn, mtd); /// 当前状态
if(stat) { /// 可以接收
if(stat == 2) {
tmpStr[charCnt] = 0;
if(mtd!=10 && mtd!=9) /// 不显示换行和空白
printf("%d {%s}\n", mtd, tmpStr);
charCnt = 0;
LA.ReStart(1); /// 一定要记得每次接收后重新开始自动机
}else if(stat == 3) {
BackChar(); charCnt --; /// 退还最后一个字符
tmpStr[charCnt] = 0;
if(mtd!=10 && mtd!=9)
printf("%d {%s}\n", mtd, tmpStr);
charCnt = 0;
LA.ReStart(1); /// 一定要记得每次接收后重新开始自动机
}
if(mtd == 11) isEnd = 1;
if(mtd == 10) {
LA.line ++;
printf(" LA.line = %d\n", LA.line);
}
}
}while(!isEnd);
#undef LA
}
int main() {
InitClassId();
LexicalAnalysis.ClearAutoMachine();
LexicalAnalysis.InputFromFile("auto.txt");
LexicalAnalysis.CheckFull();
LAtest("in.txt");
return 0;
}
用于测试的一个小文件:
Func IsPrime(Int x) As Int
If(x<=1){ /*1不是质数*/
Return 0;
}else{
If(x==0x02){ /*2是质数*/
Return 1;
}
For(Int i=2; i*i<=x; i ++){
If(x%i==0)Return 0;
}
Return 1;
}
EndFunc
1.0
0.5
1.0e-6
"Hello world!"
按照既定的语法,将词法分析得到的散乱的词汇整理成抽象语法树的形式。在这个过程中,同时检验源代码的语法是否正确。形成抽象语法树之后,我们还将把这门编程语言转化成一门类似汇编语言的中间语言。
为了清除的描述语法,我们将规定一些词汇集合,从而辅助我们定义语法。
保留字是字符串,所有保留字都符合标识符组词规则(定义见语法分析)。这门语言中保留字列举如下:
if else
for while break continue
and or not
return
int double
struct
除保留字外,其余所有符合标识符组词规则的词汇,称为用户定义标识符集合。
符合 十进制整数组词规则、十进制实数组词规则、十进制科学计数法组词规则、十六进制整数组词规则、常量字符串组词规则 五种规则中任意一种规则的词汇,称为常量。
单个常量的定义,有以下三种形式:
const [int/double] [变量名1] = [值1] ;
const [int/double] [数组名][维度1长度][维度2长度]... = { [值1], [值2], [值3], ... } ;
const [结构体名] [变量名] = { [值1], [值2], [值3], ... } ;
类型相同的多个常量可由逗号分隔后,用同一个语句定义:
const [类型名] [描述1], [描述2], [描述3], ... ;