创造新语言(2)——用Lex&Yacc构建简单的分析程序

昨天我们开始设计了一门新语言,制定了基本的开发架构,今天我们就先来了解一下,两个非常好用的工具,编译器前端构建的神器——Lex&Yacc,这两个工具在linux下叫做flex和bison。

Lex是词法分析器构建工具,我们安装对应的词法规则书写,那么就能够为我们生成对应的词法分析器,自动帮我们分好token,而分词工作,一直是编译系统的基础任务。

我们今天,先来尝试编写一个BNF语法的解析器,将我们的BNF解析成代码可以识别的数据格式,BNF的格式大概是这样:

{{ do_init() }}

# 定义列表
<definelist> = <definelist> <define> | e ;
<define> = <constdef> | <vardef> | <functiondef> ;

# 变量和常量定义
<constdef> = "const" <const_def_run>  <vardef> {{ isconstdef = false }} ;
<vardef> = "int" <iddeflist> ";" ;
<iddeflist> = <iddeflist> "," <iddef> | <iddef> ;

这里就是我们的原始输入文件了,我们希望将其解析成为内存中的树结构,供我们的编译器使用,目前的任务就是,读入这样的文本,解析成树状结构。

这里我们可以发现,这种定义方式就是扩展BNF范式,而其中添加了语义动作的脚本{{ }},脚本也可以单独使用,用来做一些初始化工作等。

利用词法分析程序处理单词

我们先看一下基本的Lex扫描器如何编写:

/* scanner.l */
%{

#include <stdio.h>
#include "parser.hpp"

#define SAVE_TOKEN yylval.str = yytext
extern "C" int yywrap() { return 1; }

%}

/* show the line number */
%option yylineno

%%

"/*"([^\*]|(\*)*[^\*/])*(\*)*"*/" ; 

#[^\n]*\n               ; /* ignore line comment */ 

[ \t\v\n\f]             ; /* ignore blank token */

\"(\\.|[^\\"])*\"       SAVE_TOKEN; return STRING;

"{{"([^}]|\}[^}])*"}}"  SAVE_TOKEN; return SCRIPT;

"e"                     return ‘e‘;

":"                     return ‘:‘; 

"<"                     return ‘<‘; 

">"                     return ‘>‘; 

"["                     return ‘[‘;

"]"                     return ‘]‘;

"="                     return ‘=‘; 

"|"                     return ‘|‘;

";"                     return ‘;‘;

[a-zA-Z_][a-zA-Z0-9_]*  SAVE_TOKEN; return ID;

%%

这里的扫描器是根据多个正则式同时匹配的原理,注意,这里的条目是有顺序的,越靠上的元素优先级越高,假若第一个能匹配上,那么就不会匹配下面的,但存在更长的匹配时,取更长的。

这里用到了非常神奇的匹配:

"/*"([^\*]|(\*)*[^\*/])*(\*)*"*/"

能够匹配 /* comment */这样的注释。

\"(\\.|[^\\"])*\"

能够匹配C风格字符串。

"{{"([^}]|\}[^}])*"}}"

能够匹配类似{{ somefunction() }}这样的脚本。

如果您对这几个正则式不理解的话,希望先看一下我写的这篇文章,相信会对您有些帮助:

【Lex识别C风格字符串和注释 】

词法分析程序使用很多内置变量和函数,我再来介绍一下这几句话的意思:

extern "C" int yywrap() { return 1; }

yywrap是一个用来处理多文件的函数,它会在一个文件处理到结尾时被调用。假若你有多个文件,希望连续的被lex处理,那么你可以开一个文件列表,然后在这里依次将对应的文件接入到lex的输入流中,并且返回0,lex就认为还没处理结束,而yywrap一旦返回1时,表示所有的任务已经完成,可以结束了。

#define SAVE_TOKEN yylval.str = yytext

这个SAVE_TOKEN宏,用到了一个yacc中的内置变量yylval,这个变量是一个联合类型,一会儿你会在yacc的文件定义中发现一个%union的定义,就是它的类型定义。这部分的具体声明,可以在yacc生成的parser.hpp的头文件中找到。

%option yylineno

这是一个参数设置,启用了lex的报错机制,能够确定对应token的具体行号,虽然肯定会消耗一点资源,但debug也是十分重要的,使用时,只要在外部引用其中的yylineno变量,就能知道当前识别到的位置的行号:

extern int yylineno;

利用Yacc识别语法

为了正确的识别整个BNF语法,并且结构化的解析他们,我们写了如下的一个yacc程序:

/* parser.y */
%{

#include <stdio.h>

extern int yylex();

extern int yylineno;
extern char* yytext;
void yyerror(const char *s);

%}

%union {

    char *str = NULL;

}

%token <str> ID STRING SCRIPT

%start list

%%
/* 总的混合bnf和脚本的列表 */
list : item
     | list item
     ;

/* 可以是bnf或脚本 */
item : bnf_item
     | SCRIPT
     ;

/* 一行bnf的定义 */
bnf_item : symbol ‘=‘ bnf_list ‘;‘
         ;

/* bnf后面的部分 */
bnf_list : symbol_list
         | bnf_list ‘|‘ symbol_list
         ;

/* 一条bnf项的列表 */
symbol_list : symbol
            | symbol_list symbol
            ;

/* 可用的bnf符号 */
symbol : ‘<‘ name ‘>‘
       | ‘[‘ name ‘]‘
       | ‘e‘
       | STRING
       | SCRIPT
       ;

/* 名字,并且可以定义实例名 */
name : ID
     | ID ‘:‘ ID
     ;
%%

void yyerror(const char* s){
    fprintf(stderr, "%s \n", s);
    fprintf(stderr, "line %d: ", yylineno);
    fprintf(stderr, "error %s \n", yytext);
}

这段程序就是在定义整个BNF语法的结构,以及按照什么样的规则规约他们,这里我们并没有添加语义动作,我们会在接下来的时间里将其添加成为一个可用的分析器。

添加主处理函数

我们的yacc和lex写的源文件可以被翻译为C++代码,但仅仅拥有一个基本的处理函数,要想处理文件,那就有自己编写文件的打开部分并将该文件重定向到yyin输入流中。


#include <stdio.h>
#include "parser.hpp"
#include "help_message.h"

extern FILE* yyin;
FILE* file_in;

int main(int argc,const char *argv[])
{
    printf("Welcome to use the XScript!\n");
    if (argc <= 1) printf(help_message);
    else {
        /* open the file and change the yyin stream. */
        const char *file_in_name = argv[1];
        if ((file_in=fopen(file_in_name,"r"))==NULL) {
            printf("error on open %s file!",file_in_name);
            getchar();
            return 1;
        }
        yyin = file_in;
        yyparse();

        /* you should close the file. */
        fclose(file_in);
    }
    return 0;
}

恩,主函数都写完了,我也想给这个项目起个名字,就先叫做XScript吧,意为多变的脚本,希望能成为一门自定义语法的翻译语言。

然后大家应该问,既然都写完了,那么如何编译构建呢?这里我们使用cmake构建整个工程,现在cmake也比较方便,能够支持直接调用lex和yacc的linux版,我们只需要增加两个cmake模块就可以实现项目的构建:

cmake_minimum_required(VERSION 2.8)

project(scanner)
SET(CMAKE_CXX_COMPILER_ENV_VAR "CXX")
SET(CMAKE_CXX_FLAGS "-std=c++11")
include_directories(include build src)

# bison and flex
find_package(BISON)
find_package(FLEX)
flex_target(SCANNER src/scanner.l  ${CMAKE_CURRENT_BINARY_DIR}/scanner.cpp)
bison_target(PARSER src/parser.y  ${CMAKE_CURRENT_BINARY_DIR}/parser.cpp)
ADD_FLEX_BISON_DEPENDENCY(SCANNER PARSER)

# src files and make exe
file(GLOB_RECURSE source_files "${CMAKE_CURRENT_SOURCE_DIR}/src/*.cpp")
add_executable(scanner
    ${source_files}
    ${BISON_PARSER_OUTPUTS}
    ${FLEX_SCANNER_OUTPUTS})

项目的文件组织目前是这样的:

LR_Scanner
    | ---- build
    | ---- src
            | ---- main.cpp
            | ---- help_message.h
            | ---- parser.y
            | ---- scanner.l
    | ---- CMakeLists.txt

好的,在build路径下:

cmake ..
make

就搞能编译通过了,但运行并没有什么效果,这只是因为语义动作并没有执行,等我们添加好语义动作后,效果就不一样了,而且目前的解析器,只要你给的语法不对,他就会在对应的位置报错,还是很方便的。

时间: 2024-10-16 14:31:33

创造新语言(2)——用Lex&Yacc构建简单的分析程序的相关文章

创造新语言(1)——确定架构

最近写C代码很不爽,感觉很多地方十分冗余,希望能够改进一下,于是诞生了这个想法,利用学过的编译原理的前端知识,构建一门新语言的编译器,然后将新语言翻译成为标准的C89. 这样,即保障了可用性,同时又大大减轻了后端的工作量,何乐而不为呢? 我说明一下选择C89的原因,首先,C语言的扩展不多,主要是C++和Objective-C.编译到的目标语言平台,如果是C++,太重了,难以做操作系统级的编程,但如果想让编译器设计的十分轻巧,那么就要尽量回避重型语言. Java也是一个不错的选择,但其平台上已经有

创造新语言(3)——添加语义处理程序

好久没有更新了,主要是研究了一段时间的C++的面向对象的设计方式和更好的架构程序的思路.其实今天这部分代码早已完成,但希望能够更好的给大家讲解如何做一个可用的新编程语言. 上次说到Bison的语义分析功能,但并没有添加对应的语义处理功能,我们这次就构建一个描述语法的抽象语法树. 首先Bison的语义处理功能是十分方便的,只要在后面添加C++的语义动作代码就可以了,但注意,$$ $1 $2 $3则是内部变量,$$表示归结元素,其余表示产生式中的元素,(脚本也是其中的$). 而这些元素具体是什么变量

[MFC]_在vs2019中使用MFC快速构建简单windows窗口程序

微软基础类库(英语: Classes,简称MFC)是微软公司提供的一个类库(class libraries),以C++类的形式封装了Windows API,并且包含一个应用程序框架,以减少应用程序开发人员的工作量.其中包含大量Windows句柄封装类和很多Windows的内建控件和组件的封装类. vs 2019 最新版,在设计上又有了很大的变化,并且其所有的服务,模块都是自定义搭建的,所以在一开始安装的时候,没有勾选mfc模块的话,是无法快速构成mfc应用的. vs2019下MFC模块的安装:

Java8新语言特性

Java8简明指南 欢迎来到Java8简明指南.本教程将一步一步指导你通过所有新语言特性.由短而简单的代码示例,带你了解如何使用默认接口方法,lambda表达式,方法引用和可重复注解.本文的最后你会熟悉最新的API的变化如Stream,Fcuntional,Map API扩展和新的日期API. 接口的默认方法 在Java8中,利用default关键字使我们能够添加非抽象方法实现的接口.此功能也被称为扩展方法,这里是我们的第一个例子: interface Formula { double calc

PERL/LEX/YACC技术实现文本解析--XML解析

继周六的p_enum.pl后,再来一篇说说我用perl做的lex,yacc工具.之前说了,我学习lex和yacc的最初动机是为了做个C语言解释器的SHELL:但后来工作中的实际需要也是制作perl版lex和yacc的一个动机.Perl库里有lex和yacc,我没研究过,想来应该比我做的强大,不过对新手来说,未必能容易入手. 我的第一个应用场景是做一个xml配置文件的排序.XML是标签标记语言,同一级下,TAG顺序本身是无所谓的:但对于测试工作来说,经常要通过文本比较工作来确定两个配置文件差别.如

Lex+YACC详解

1. 简介 只要你在Unix环境中写过程序,你必定会邂逅神秘的Lex&YACC,就如GNU/Linux用户所熟知的Flex&Bison,这里的Flex就是由Vern Paxon实现的一个Lex,Bison则是GNU版本的YACC.在此我们将统一称呼这些程序为Lex和YACC.新版本的程序是向上兼容的(译注:即兼容老版本),所以你可以用Flex和Bison来尝试下我们的实例. 这些程序实用性极广,但如同你的C编译器一样,在其主页上并没有描述它们,也没有关于怎样使用的信息.当和Lex结合使用时

Swift 新语言开发

全书目录: 一.Welcome to Swift 二.Language Guide 三.Language Reference /* 译者的废话: 几个小时前熬夜看了WWDC,各种激动,今年很有料啊!当看到Swift出来的时候,瞬间傻眼,又要学习新语言了.这篇文章来自苹果官方的<The Swift Programming Language>一书,500页左右,在苹果官网有下载.Swift大家都没实际用过,本翻译一定是有各种错漏的,各位多多包涵,我会不断更新修正的. --(博客园.新浪微博)葛布林

新语言学习

用过的语言按时序算:c++.lua(粗浅脚本).erlang.c#(unity).lua.go.印象最深的是erlang,因为特别吃亏,嗯. 那会连多线程.多进程都不怎么了解,虽然看了erlang的进程通信模型,mail_box.gen_server原理,但不理解什么时候要它们.还有递归为主的编码方式,也不大习惯. …… 以上不是本是要说的重点:技术上的东西,时间够,多用用就熟络了,且项目组对新人也有足够的宽容度. 更紧要的是那些“安全性”上的东西,尤其对已发布的项目而言. 自己以往编码,把几乎

C# 3.0新语言特性和改进(一)

引言 关于C#3.0的特性,园子里已经有了一大把,可能大家都很熟悉了,虽然本人开发中使用过,但自己还是需要记录一下,总结一下.同时也是后面写Linq知识的基础.希望有兴趣的朋友,可以看看. C# 3.0新语言特性和改进包括: 自动属性 隐含类型局部变量 匿名类型 对象与集合初始化器 扩展方法 Lambda表达式和Lambda表达式树 1. 自动属性 自动属性可以避免我们编写实体类时声明私有变量和get/set的逻辑,取而代之的是,编译器会自动为你生成一个私有变量和默认的get/set 操作. 在