zoukankan      html  css  js  c++  java
  • lex和yacc学习

    main.h文件

    #ifndef MAIN_HPP
    #define MAIN_HPP
    
    #include <iostream>//使用C++库
    #include <string>
    #include <stdio.h>//printf和FILE要用的
    
    using namespace std;
    
    /*
     * 当lex每识别出一个记号后,是通过变量yylval向yacc传递数据的。默认情况下yylval是int类型,也就是只能传递整型数据。
     * yylval是用YYSTYPE宏定义的,只要重定义YYSTYPE宏,就能重新指定yylval的类型(可参见yacc自动生成的头文件yacc.tab.h)。
     * 在我们的例子里,当识别出标识符后要向yacc传递这个标识符串,yylval定义成整型不太方便
     * (要先强制转换成整型,yacc里再转换回char*)。这里把YYSTYPE重定义为struct Type,可存放多种信息
     */
    
    /* 通常这里面每个成员,每次只会使用其中一个,一般是定义成union以节省空间(但这里用了string等复杂类型造成不可以) */
    struct Type {
        string m_sId;
        int m_nInt;
        char m_cOp;
    };
    
    /* 把YYSTYPE(即yylval变量)重定义为struct Type类型,这样lex就能向yacc返回更多的数据了 */
    #define YYSTYPE Type
    
    #endif
    

    lex.l文件

    %{
    
    /*
     * 本lex的生成文件是lex.yy.c
     * lex文件由3段组成,用2个%%行把这3段隔开
     * 
     * 第1段是声明段,包括:
     * 1-C代码部分:include头文件、函数、类型等声明,这些声明会原样拷到生成的.c文件中。
     * 2-状态声明,如%x COMMENT。
     * 3-正则式定义,如digit ([0-9])。
     * 
     * 第2段是规则段,是lex文件的主体,包括每个规则(如 identifier)是如何匹配的,以及匹配后要执行的C代码动作。
     * 
     * 第3段是C函数定义段,如yywrap()的定义,这些C代码会原样拷到生成的.c文件中。该段内容可以为空
     */
    
    /* 第1段:声明段 */
    #include "main.h"      /* lex和yacc要共用的头文件,里面包含了一些头文件,重定义了YYSTYPE */
    #include "yacc.tab.h"  /* 用yacc编译yacc.y后生成的C头文件,内含%token、YYSTYPE、yylval等定义(都是C宏),供lex.yy.c和yacc.tab.c使用 */
    
    /* 
     * 为了能够在C++程序里面调用C函数,必须把每一个需要使用的C函数,其声明都包括在extern "C"{}块里面,
     * 这样C++链接时才能成功链接它们。extern "C"用来在C++环境下设置C链接类型。
     */
    extern "C" // yacc.y中也有类似的这段extern "C",可以把它们合并成一段,放到共同的头文件main.h中
    {   
        int yywrap(void);
        int yylex(void);//这个是lex生成的词法分析函数,yacc的yyparse()里会调用它,如果这里不声明,生成的yacc.tab.c在编译时会找不到该函数
    }
    
    %}
    
    /*
     * lex的每个正则式前面可以带有"<状态>",例如下面的"<COMMENT>
    "。每个状态要先用%x声明才能使用。
     * 当lex开始运行时,默认状态是INITIAL,以后可在C代码里用"BEGIN 状态名;"切换到其它状态(BEGIN是lex/yacc内置的宏)。
     * 这时,只有当lex状态切换到COMMENT后,才会去匹配以<COMMENT>开头的正则式,而不匹配其它状态开头的。
     * 也就是说,lex当前处在什么状态,就考虑以该状态开头的正则式,而忽略其它的正则式。
     * 其应用例如,在一段C代码里,同样是串"abc",如果它写在代码段里,会被识别为标识符,如果写在注释里则就不会。
     * 所以对串"abc"的识别结果,应根据不同的状态加以区分。
     * 本例子需要忽略掉文本中的行末注释,行末注释的定义是:从某个"//"开始,直到行尾的内容都是注释。其实现方法是:
     * 1-lex启动时默认是INITIAL状态,在这个状态下,串"abc"会识别为标识符,串"123"会识别为整数等。
     * 2-一旦识别到"//",则用BEGIN宏切换到COMMENT状态,在该状态下,abc这样的串、以及其它字符会被忽略。
     * 只有识别到换行符
    时,再用BEGIN宏切换到初始态,继续识别其它记号。
     */
     
    %x COMMENT
    
    nondigit    ([_A-Za-z])   /* 非数字由大小写字母、下划线组成 */
    digit       ([0-9])       /* 一位数字,可以是0到9 */
    integer     ({digit}+)    /* 整数由1至多位数字组成 */
    identifier  ({nondigit}({nondigit}|{digit})*)  /* 标识符,以非数字开头,后跟0至多个数字或非数字 */
    blank_chars ([ f
    	v]+)  		       /* 一个或一段连续的空白符 */
    
    /*下面%%后开始第2段:规则段*/
    
    %%
    
    // 匹配标识符串,此时串值由yytext保存
    {identifier} {
        /* 通过yylval向yacc传递识别出的记号的值,由于yylval已定义为struct Type,
         * 这里就可以把yytext赋给其m_sId成员,到了yacc里就可以用$n的方式来引用了 */
        yylval.m_sId = yytext; 
        return IDENTIFIER;      // 向yacc返回: 识别出的记号类型是IDENTIFIER
    }
    
    // 匹配整数串
    {integer} {    
        yylval.m_nInt = atoi(yytext);    // 把识别出的整数串,转换为整型值,存储到yylval的整型成员里,到了yacc里用$n方式引用
        return INTEGER;           	     // 向yacc返回: 识别出的记号类型是INTEGER
    }
    
    // 遇空白符时,什么也不做,忽略它们
    {blank_chars}    {}
          
    // 遇换行符时,忽略之	  
    
            {}    
    
    // 遇到串"//",表明要开始一段注释,直到行尾
    "//" {  
        cout << "(comment)" << endl;    // 提示遇到了注释
        BEGIN COMMENT; 	            // 用BEGIN宏切换到注释状态,去过滤这段注释,下一次lex将只匹配前面带有<COMMENT>的正则式
    }
    
    // .表示除
    以外的其它字符,注意这个规则要放在最后,因为一旦匹配了.就不会匹配后面的规则了(以其它状态<>开头的规则除外)
    . {
        yylval.m_cOp = yytext[0];	// 由于只匹配一个字符,这时它对应yytext[0],把该字符存放到yylval的m_cOp成员里,到了yacc里用$n方式引用
        return OPERATOR;		// 向yacc返回: 识别出的记号类型是OPERATOR
    }
    
    // 注释状态下的规则,只有当前切换到COMMENT状态才会去匹配
    <COMMENT>
     {    
        BEGIN INITIAL;	// 在注释状态下,当遇到换行符时,表明注释结束了,返回初始态
    }
    
    // 在注释状态下,对其它字符都忽略,即:注释在lex(词法分析层)就过滤掉了,不返回给yacc了
    <COMMENT>.    {}
    
    %%
    
    // 第3段:C函数定义段
    int yywrap(void)
    {
        puts("-----the file is end");
        return 1;	//返回1表示读取全部结束。如果要接着读其它文件,可以这里fopen该文件,文件指针赋给yyin,并返回0
    }
    

    yacc.y文件

    %{
    
    /*
     * 本yacc的生成文件是yacc.tab.c和yacc.tab.h
     * yacc文件由3段组成,用2个%%行把这3段隔开。
     *
     * 第1段是声明段,包括:
     * 1-C代码部分:include头文件、函数、类型等声明,这些声明会原样拷到生成的.c文件中。
     * 2-记号声明,如%token
     * 3-类型声明,如%type
     *
     * 第2段是规则段,是yacc文件的主体,包括每个产生式是如何匹配的,以及匹配后要执行的C代码动作。
     *
     * 第3段是C函数定义段,如yyerror()的定义,这些C代码会原样拷到生成的.c文件中。该段内容可以为空
     */
    
    /* 第1段:声明段 */
    #include "main.h"	// lex和yacc要共用的头文件,里面包含了一些头文件,重定义了YYSTYPE
    
    /*
     * 为了能够在C++程序里面调用C函数,必须把每一个需要使用的C函数,其声明都包括在extern "C"{}块里面,
     * 这样C++链接时才能成功链接它们。extern "C"用来在C++环境下设置C链接类型。
     */
    extern "C"
    {    //lex.l中也有类似的这段extern "C",可以把它们合并成一段,放到共同的头文件main.h中
        void yyerror(const char *s);
        extern int yylex(void);	//该函数是在lex.yy.c里定义的,yyparse()里要调用该函数,为了能编译和链接,必须用extern加以声明
    }
    
    %}
    
    /*
     * lex里要return的记号的声明
     * 用token后加一对<member>来定义记号,旨在用于简化书写方式。
     * 假定某个产生式中第1个终结符是记号OPERATOR,则引用OPERATOR属性的方式:
     * 1-如果记号OPERATOR是以普通方式定义的,如%token OPERATOR,则在动作中要写$1.m_cOp,以指明使用YYSTYPE的哪个成员
     * 2-用%token<m_cOp>OPERATOR方式定义后,只需要写$1,yacc会自动替换为$1.m_cOp
     * 另外用<>定义记号后,非终结符如file, tokenlist,必须用%type<member>来定义(否则会报错),
     * 以指明它们的属性对应YYSTYPE中哪个成员,这时对该非终结符的引用,如$$,会自动替换为$$.member
     */
    
    %token<m_nInt>		INTEGER
    %token<m_sId>		IDENTIFIER
    %token<m_cOp>		OPERATOR
    %type<m_sId>		file
    %type<m_sId>		tokenlist
    
    %%
    
    // 文件,由记号流组成
    file:    
        tokenlist    //这里仅显示记号流中的ID
        {
            /* $1是非终结符tokenlist的属性,由于该终结符是用%type<m_sId>定义的,即约定对其用YYSTYPE的m_sId属性,
             * $1相当于$1.m_sId,其值已经在下层产生式中赋值(tokenlist IDENTIFIER) */
            cout<<"all id:"<<$1<<endl;    
        };
    	
    // 记号流,或者为空,或者由若干数字、标识符、及其它符号组成
    tokenlist:
        {
        }
        | tokenlist INTEGER
        {
    	/* $2是记号INTEGER的属性,由于该记号是用%token<m_nInt>定义的,即约定对其用YYSTYPE的m_nInt属性,$2会被替换为yylval.m_nInt,已在lex里赋值 */
            cout<<"int: "<<$2<<endl;
        }
        | tokenlist IDENTIFIER
        {
            /* $$是非终结符tokenlist的属性,由于该终结符是用%type<m_sId>定义的,即约定对其用YYSTYPE的m_sId属性,$$相当于$$.m_sId,
             * 这里把识别到的标识符串保存在tokenlist属性中,到上层产生式里可以拿出为用 */
            $$+=" " + $2;
    	// $2是记号IDENTIFIER的属性,由于该记号是用%token<m_sId>定义的,即约定对其用YYSTYPE的m_sId属性,$2会被替换为yylval.m_sId,已在lex里赋值
            cout << "id: "<< $2 <<endl; 
        }
        | tokenlist OPERATOR
        {
    	// $2是记号OPERATOR的属性,由于该记号是用%token<m_cOp>定义的,即约定对其用YYSTYPE的m_cOp属性,$2会被替换为yylval.m_cOp,已在lex里赋值
            cout<<"op: "<<$2<<endl;
        };
    
    %%
    
    // 当yacc遇到语法错误时,会回调yyerror函数,并且把错误信息放在参数s中
    void yyerror(const char *s)    
    {
        cerr<<s<<endl;//直接输出错误信息
    }
    
    int main()//程序主函数,这个函数也可以放到其它.c, .cpp文件里
    {
        const char* sFile="file.txt";//打开要读取的文本文件
        FILE* fp=fopen(sFile, "r");
        if(fp==NULL)
        {
            printf("cannot open %s
    ", sFile);
            return -1;
        }
        extern FILE* yyin;    //yyin和yyout都是FILE*类型
        yyin=fp;//yacc会从yyin读取输入,yyin默认是标准输入,这里改为磁盘文件。yacc默认向yyout输出,可修改yyout改变输出目的
    
        printf("-----begin parsing %s
    ", sFile);
        yyparse();//使yacc开始读取输入和解析,它会调用lex的yylex()读取记号
        puts("-----end parsing");
    
        fclose(fp);
    
        return 0;
    }
    

    ** 参考:**
    https://www.cnblogs.com/rednodel/p/4500388.html

  • 相关阅读:
    C# 序列化与反序列化之DataContract与xml对子类进行序列化的解决方案
    C# 序列化与反序列化之Binary与Soap无法对泛型List<T>进行序列化的解决方案
    大端小端存储方案
    C# 序列化与反序列化Serialization之Json Xml Binary Soap JavaScript序列化
    C# ctpclient networkstream 使用 BinaryReader的ReadString但是使用streamReader的Readtoend不行
    C# TcpListener TcpClient
    C# System.Net.Mail.MailMessage 发邮件
    C# System.Web.Mail.MailMessage 发邮件
    离线环境下使用二进制方式安装配置Kubernetes集群
    Kubernetes基础:查看状态、管理服务
  • 原文地址:https://www.cnblogs.com/goya/p/14012663.html
Copyright © 2011-2022 走看看