写一个不需要脑子的解释器

-1.前言

什么,你要写AC自动机?什么,你要学编译原理?老哥你在逗我吗?我写解释器就是为了休闲娱乐,自己乱搞,搞得那么专业干什么?

曾经写过一个小巧的“解释器”,解释的是一门我自己YY出来的垃圾语言,虽说功能奇弱,不过从语法上讲也算得上要if有if,要while有while,足够用来计算一些简单的小学数学题,比如,判断个闰年、打印个一万以内的质数表之类的(欧拉筛?想得美,我没有数组…)。某C#资深大神看了我上一个版本解释器的C++源代码之后,深感代码丑陋,逻辑性极差,不堪卒读(请同学们注意,成语“不堪卒读”和“不忍卒读”在含义上有着微妙地差异),缺少解释器所应具备的大多数组成成分。为此,我打算重新写一个…(我还真是个“倔强(youqu)”的人啊。)

0.暴力载满,愉悦身心。

好了,作为一个热爱信竞(baoli)的孩子,我们一同踏上了一条静态查错的不归路。警告:下文内容纯属胡扯,如果有人因为信了我的话,导致了不良后果(比如挂科之类的),我绝不负责。解释的语言仍是我自己YY出来的,但与之前的相比有所不同。

I.代码中使用的命名规则

  1. 全部标识符 一律采用驼峰式命名法。
  2. 变量、命名空间(namespace)名首字母一律小写。
  3. 自定义的常量、类名、结构体名、函数名、首字母一律大写(函数名main除外)。

II.注释规则

  1. 函数定义所在行应有解释函数功能的注释。
  2. return所在行应有解释return内容的注释。

1.词法分析

把混乱的文件(用暴力的方法),拆成单词的组合,从而提升文件的有序性。我打算浪费一点硬盘空间和处理时间,每一步处理时,都生成出一个处理中间文件,用以分析程序错误。下一步的处理,将会利用到上一步生成的中间文件。

在实际操作之前,我们需要做一些必要的定义。

I.为字符分类

由于我打算模糊化对汉字等特殊符号的处理,所以,在这里我们姑且认为一个字节(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; /// 理论上不会出现找不到字符分类的情况 
}

验证这个程序的正确性,尽管这么智障的程序,我们也要严谨。(验证程序略)

II.组词规则

由于我懒得学习正则表达式,组词规则将用自然语言或代码描述,尽管自然语言描述肯能并不严谨。

  1. 标识符组词规则:
    意义:标识符组词规则,规定了关键字、以及用户自己定义的函数名、变量名、常量名、结构体名的格式标准。
    内容:标识符是一个以下划线、小写字母或大写字母开头的字符串,字符串中可以包含下划线、小写字母、大写字母和数字,而不能此外的其他字符。
    标识符之后的第一个字符只能是空白符、算符或文件结尾。
  2. 十进制整数组词规则:
    十进制整数是一个以数字为开头的字符串,字符串中只可以包含数字
    十进制整数之后的第一个字符只能是空白符、算符(除点号(即ASC(46))外)或文件结尾。
  3. 十进制实数组词规则:
    十进制实数是一个以数字为开头的字符串,字符串中可以包含数字和小数点(即ASC(46)),而不能包含其他字符,且小数点在字符串中出现且仅出现一次
    十进制实数之后的第一个字符只能是空白符、算符(除点号(即ASC(46))外)或文件结尾。
  4. 十进制科学计数法组词规则:
    十进制科学计数法是一个字符串,这个字符串可以无隙拆分为首尾顺次相接的三个非空子串。它们依次具有以下性质:第一部分,符合十进制实数组词规则。第二部分只有六种可能,它们是 “e”,“e+”,“e-”,“E”,“E+”,“E-”(不含双引号)。第三部分,符合十进制整数组词规则。
    十进制科学计数法之后的第一个字符只能是空白符、算符(除点号(即ASC(46))外)或文件结尾。
  5. 十六进制整数的组词规则:
    十六进制整数是一个以0x为前缀的、长度大于等于3的字符串,除前两个字符外,这个字符串剩余的部分只能包含以下的22种字符:数字,小写字母a~
    f,大写字母A~F

    十六进制整数之后的第一个字符只能是空白符、算符(除点号(即ASC(46))外)或文件结尾。
  6. 常量字符串组词规则:
    常量字符串是一个长度大于等于2的字符串,这个字符串的第一个字符和最后一个字符都是英文双引号(即ASC(34)),其余字符可以是除双引号、回车符(即ASC(13))、换行符(即ASC(10))和文件尾(即ASC(-1))外的任何字符。
  7. 运算符组词规则:
    运算符是一个字符串,若这个字符串是以下21个字符串之一,则它符合算符组词规则:( ) [ ] . { } ; + - * / % = > >= < <= ^ == <>。(确实是21个,如果你能数明白,说明你能理解我的意思。)(同时注意区分“算符”和“运算符”的概念。)
  8. 注释组词规则:
    注释是一个长度大于等于4的字符串,“/*”是这个字符串的一个前缀,“*/”是这个字符串的一个后缀,除了字符串的最后两个字符外,这个字符串中不含有子串“*/”。
  9. 空白组词规则:
    空白是一个非空字符串,这个字符串中只能含有空格(ASC(32))和Tab(ASC(9))两种字符。(注意区分“空白”和“空白符”的涵义。)
    要求空白后的第一个字符不能是空白符。
  10. 换行组词规则:
    换行是一个字符串,这个字符串可以无隙拆分成两部分。第一部分,符合空白组词规则。第二部分只有三种可能性,“\13\10”,“\13”,“\10”。(p.s.:“\x(x为整数)”表示ASCII码为x的字符。)(注意区分“换行”与“换行符”的涵义。)
    对换行后的第一个字符要求比较特殊,若换行的最后一个字符为换行符(ASC(10)),对其后的第一个字符没有要求。若换行的最后一个字符为回车(ASC(13)),则要求换行后的第一个字符不能为换行符(ASC(10))。
  11. 文件结尾组词规则:
    一个仅含一个字符的字符串,且这个字符为EOF(即ASC(-1))。

III.词法中用到的程序段

不难发现,以上词法分析过程可以通过手动构造自动机轻松实现(我说我不想写AC自动机,不代表我不手动构造自动机)。

先给出手稿,具体信息留坑待补:

词法分析机

这个词法分析自动机的正确性、严谨性是有待考证的,因此先不给出自动机的数据。(tips:有的else边可能没写上。)

a. 字符退还功能的实现

写一个字节的伪缓冲区即可。

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; /// 退还最后一次读出的字符 
    }
}

b.得到的词法单元的表示方式

我们可以用一个三元组来描述得到的词法单元:(组词规则编号,串长度,原内容字符串)。实际上,在词法分析过程中,我们可以丢弃注释和空白。但我们不能丢弃换行,因为换行决定着当前光标在文件中所处的行数,行数在错误信息的输出中有重要意义。

c.自动机的描述方式

对自动机的描述可以分为两部分,对点的描述,对边的描述。

  1. 对点的描述:
    结点主要分为四类:普通结点(0)、报错结点(1)、接收结点(2)、退还接收结点(3)
    报错结点:需要注明错误原因,输出错误原因时需要输出出错位置。错误位置可用如下三元组描述:(当前文件名,行数,出错前扫描到的最后一个字符)。
    接收结点/退还接受结点:需要注明接收到的字符串的类型(也就是采用了哪种组词规则)。
    同时,我们还需要为自动机的每一个结点分配一个独一无二的编号。
  2. 对边的描述:
    边需要记录三个信息,起点、终点以及能与这条边匹配的字符集合。同点一样,我们也需要为自动机的每一条边分配一个互不重复的编号。为了便于描述每条边所能匹配的字符集合,我们规定了以下几个集合:
编号集合名称集合内容
10CLU 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 CapitalCharLowerCharUnderLine
11SP { 32 , 9 } \{32,9\} {32,9}
12NNumeralChar完全相同
13NZ [ 49 , 57 ] [49,57] [49,57] (即非零数字)
14SOE 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\} SpaceCharOperatorEndOfFile{46}
15dot { 46 } \{46\} {46}
16NAF [ 48 , 57 ] ∪ [ 97 , 102 ] ∪ [ 65 , 70 ] [48,57]\cup [97,102] \cup [65,70] [48,57][97,102][65,70]
17OUS { 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很多组词规则,要求组词结束后的第一个字符属于这一集合。(注:这一集合并不包含dotASC(46)))
dot这个集合没必要定义,我纯属闲的。其中只含有一个字符——点(也就是英文句号)。
NAF数字、a~f、A~F,这是十六进制数中常出现的元素集合。
OUSOperators 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
    }
}

根据以上的内容,在此制定用文本文件描述自动机信息的格式:

  • 第一行写有两个用空格分隔的十进制非负整数 N , M N, M N,M,分别表示这个自动机的所有点的编号的集合为 V = { 1 , 2 , . . , N } V=\{1, 2, .., N\} V={1,2,..,N},所有边的编号的集合为 E = { 1 , 2 , . . , M } E=\{1, 2, .., M\} E={1,2,..,M}
  • 接下来的 N N N 行,每行用于描述自动机中的一个结点。(为了便于纠错)行首包含一个正整数 v v v v ∈ V v \in V vV,且每行读入的 v v v互不相同),表示当前被描述结点的编号。其后用空格隔开,有一个正整数 t t t t = 0 t=0 t=0表示当前结点为普通结点, t = 1 t=1 t=1表示当前结点为报错结点, t = 2 t=2 t=2表示当前结点为接收结点, t = 3 t=3 t=3表示当前结点为退还接收结点,同时要求 t ∈ { 0 , 1 , 2 , 3 } t\in\{0, 1, 2, 3\} t{0,1,2,3}。若 t = 1 t=1 t=1其后用一个字符串描述错误信息,方便起见,要求错误信息中不含空白符。若 t = 2 或 3 t=2或3 t=23其后用一个整数描述接收到的字符串符是按照哪一种组词规则接收的。
  • 接下来的 M M M 行,每行用于描述自动机中的一条边。(为了便于纠错)行首包含一个正整数 e e e e ∈ E e \in E eE,且每行读入的 e e e互不相同),表示当前被描述的边的编号。其后,有两个用空格隔开的正整数 p , q p,q p,q,表示这条边由 p p p指向 q q q(其中 p ∈ V , q ∈ V p\in V, q\in V pV,qV)。再后,有一个字符串 i i i。若字符串 i i i符合十进制整数组词规则,我们认为这条边能够匹配以 i i i的数值为ASCII码的那个字符。若字符串 i i i"CLU","SP","N","NZ","SOE","dot","NAF","OUS","else"这九个字符串之一(不含双引号和逗号),则认为这条边能够匹配,以字符串 i i i为名字的字符集合中的任意一个字符。

d.字典树模板类的实现

这种字典树在整个解释器的实现过程中有着很多的用处,在这里,我们用它来实现集合名字符串与集合编号的转换。


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);
}

e.自动机模板类的实现

不像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!"

2.语法分析

按照既定的语法,将词法分析得到的散乱的词汇整理成抽象语法树的形式。在这个过程中,同时检验源代码的语法是否正确。形成抽象语法树之后,我们还将把这门编程语言转化成一门类似汇编语言的中间语言。

为了清除的描述语法,我们将规定一些词汇集合,从而辅助我们定义语法。

I.词汇集合

a.保留字集合

保留字是字符串,所有保留字都符合标识符组词规则(定义见语法分析)。这门语言中保留字列举如下:

if      else
for     while   break    continue
and     or      not
return
int     double
struct

b.用户定义标识符集合

除保留字外,其余所有符合标识符组词规则的词汇,称为用户定义标识符集合。

c.常量集合

符合 十进制整数组词规则、十进制实数组词规则、十进制科学计数法组词规则、十六进制整数组词规则、常量字符串组词规则 五种规则中任意一种规则的词汇,称为常量。

II.定义语法

a.常量的定义

单个常量的定义,有以下三种形式:

const [int/double] [变量名1] = [值1] ;

const [int/double] [数组名][维度1长度][维度2长度]... = { [值1], [值2], [值3], ... } ;

const [结构体名] [变量名] = { [值1], [值2], [值3], ... } ;

类型相同的多个常量可由逗号分隔后,用同一个语句定义:

const [类型名] [描述1], [描述2], [描述3], ... ;

未完待续 …

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值