实验 1 词法与语法分析器

在本实验中,你将通过编写文法描述文件,使用 ANTLR v4 生成词法和语法分析器,并编写一个在 parse tree 上遍历生成 abstract syntax tree (AST) 的 visitor 来实现一个 C1 语言分析器。

本次实验分三次提交,实验内容及提交要求见 此处

  • lab1-1:构造 C1 的词法分析器
  • lab1-2:构造 C1 的语法分析器
  • lab1-3:构造能生成 AST 的 C1 解析器

C1 语言说明

C1 是本课程实验要实现的一个 C 语言子集。它没有完善的类型系统,只有整型和整型数组,并附带 const 描述符;它的函数定义没有参数和返回值。C1语言的文法采用 EBNF (Extended Backus-Naur Form) 表示如下:

CompUnit    → [ CompUnit ] ( Decl | FuncDef ) 
Decl        → ConstDecl | VarDecl
ConstDecl   → const int ConstDef { , ConstDef } ';'
ConstDef    → ident '=' Exp | ident '[' [ Exp ] ']' '=' '{' Exp { ',' Exp } '}'
VarDecl     → int Var { , Var } ';'
Var         → ident | ident '[' Exp ']' | ident '=' Exp
            | ident '[' [ Exp ] ']' '=' '{' Exp { ',' Exp } '}'
FuncDef     → void ident '(' ')' Block
Block       →'{' { BlockItem } '}'
BlockItem   → Decl | Stmt
Stmt        → LVal '=' Exp ';' | ident '(' ')' ';' | Block 
            | if '( Cond ')' Stmt [ else Stmt ] | while '(' Cond ')' Stmt | ';'
LVal        → ident | ident '[' Exp ']'
Cond        → Exp RelOp Exp
RelOp       →'==' | '!=' | '<' | '>' | '<=' | '>='
Exp         → Exp BinOp Exp | UnaryOp Exp | '(' Exp ')' | LVal | number
BinOp       → '+' | '−' | '*' | '/' | '%'
UnaryOp     → '+' | '−'
EBNF 中的符号含义
  • [...]: 内包含的为可选项
  • {...}: 内包含的为可重复0次至无数次的项

文法之外,一些需要补充的内容有:

  • C1 语言中运算符的优先级和 C 语言一致。
  • C1 语言的注释和 C 语言一致:行注释为//开始,到第一个行尾字符非\的行结束的区域;块注释为从/*开始,到第一个*/处结束的区域。
  • 数值允许十进制和十六进制整型。

实验内容与提交要求

在本课程实验中,你将通过编写文法描述文件和相关的处理代码,使用 ANTLR v4,最终为C1 语言实现一个能输出抽象语法树的 C1 解析器。

为便于过程控制与管理,课程实验拆分成三步,分三次提交:

  • lab1-1:构造 C1 的词法分析器
  • lab1-2:构造 C1 的语法分析器
  • lab1-3:构造能生成 AST 的 C1 解析器

你需要对所提供的课程实验软件包中的部分文件进行修改和完善,补充更多的测试例子,撰写和提交介绍课程实验的分析、设计和测试方面的报告。你需要事先阅读本页其他小节的内容,它们将能在一定程度上给你的课程实验提供帮助和指引。

如果有疑问请及时提出,我们会公开回答有必要回答的问题。

Lab1-1. C1 的词法分析

主要任务

根据 C1 语言 的词法描述,修改并完善课程实验软件包中 C1 的词法描述文件c1recognizer/grammar/C1Lexer.g4,利用 ANTLR v4 节介绍的antlr4grun工具编译和测试你的分析器。请编写若干个正确的和错误的C1程序作为测试程序,来测试你所构造的C1 词法分析器。测试输入应置于test/test_cases/lexer中。

重点与难点

  • 请在 Lexer 实现中特别注意行尾的处理,正确地处理 CRLF 和 LF 两种换行
  • 你需要仔细编写单行注释和多行注释的词法规则,并构造各种测试例子进行测试

提交说明

你需要参照实验软件包 c1recognizer 的目录结构来组织你本次实验的提交文件,具体如下:

- <your repo>
  | c1recognizer         复制自公共仓库的 c1recognizer 项目。请勿遗漏内容。
    | cmake/
>>  | grammar/           你需要修改其中的 C1Lexer.g4
    | include/c1recognizer/ 
    | src/
>>  | test/test_cases/   你需要在其中补充你的测试程序
>>  | doc/               你需要在其中增加文档描述你在实验中遇到的问题、你的分析和设计,文件名的前缀为`lab1-1`
    | 其他已有的文件

Lab1-2. C1 的语法分析

主要任务

根据 C1语言 的 EBNF 描述,修改并完善课程实验软件包中 C1 的语法描述文件c1recognizer/grammar/C1Parser.g4,利用 ANTLR v4 节介绍的antlr4grun工具编译和测试你的分析器。请编写若干个正确的和错误的C1程序作为测试程序,来测试你所构造的C1 语法分析器。测试输入应置于test/test_cases/中。 对 const 修饰的语义处理我们会在下次实验中详细说明,本次实验中只需要构建出正确的 AST 树即可。

重点与难点

分析和总结 ANTLR 对左递归文法的处理方法,回答以下问题:

  • ANTLR 容许哪些类型的左递归?
  • ANTLR 对所支持的左递归如何处理?例如,对下面两种情况分别会怎样解析?
    • e : e '*' e # Mult | e '+' e # Add | INT # Int ;
    • e : e '+' e # Add | e '*' e # Mult | INT # Int ;
  • 给出ANTLR 不支持的左递归文法的例子并尝试分析原因。

测试时,你可以使用 Lexer 和 Parser 混合的 grammar 文件,会更加方便。

测试时,需要你脱离grun工具进行测试,自行编写主体工作程序。你可以参考公共仓库中的 antlr-example,这一例子以 Python 作为工作语言从而十分简洁方便,你也可以使用其它语言,如 Java、C#、C++等。其中 C++ 可能会由于 antlr4cpp 的使用困难遇到一些阻碍,如果决定使用 C++ 你可以参考 c1reognizer 项目进行构建。

提交说明

你需要在 Lab1-1 的基础上修改并组织你本次实验的提交文件,具体如下:

- <your repo>
  | lab-1-2-answer       你需要在其中添加用于回答左递归问题的相关文法,包括 MultFirst.g4 PlusFirst.g4 UnsupportedLeftRecursive.g4;
                         以及调用这些文法的测试程序、和关于你的程序的说明,包括依赖环境、编译方式、使用方式、分析内容与结论。
                         如果需要单独的编译过程最好提供 Makefile 或 CMakeLists.txt。
                         除了测试程序和 grammar 文件外,你还需要在这其中对"重点与难点"小节中的问题进行回答。
  | c1recognizer         复制自公共仓库的 c1recognizer 项目。请勿遗漏内容。
    | cmake/
>>  | grammar/           你需要修改其中的 C1Parser.g4
    | include/c1recognizer/ 
    | src/
>>  | test/test_cases/   你需要在其中补充你的测试程序
>>  | doc/               你需要在其中增加文档描述你在实验中遇到的问题、你的分析和设计。
    | 其他已有的文件

Lab1-3. 生成 AST 的 C1 解析器

主要任务

你将在前两步实验结果的基础上,修改并完善课程实验软件包中 的 src/syntax_tree_builder.cpp并将src/recognizer.cpp中第 44 行代码更改为以compilationUnit作为开始符号来解析。你需要使用编译链接生成的可执行程序 c1r_test 来测试你的分析器;测试样例同 Lab1-2,请确保至少在你的样例下,你的 AST 生成正确。

请确保你没有修改课程实验软件包中除 grammar/C1Lexer.g4grammar/C1Parser.g4src/syntax_tree_builder.cppsrc/recognizer.cpptest/test_cases中以外的文件。在提交前,通过git status检查提交涉及的文件将会是一个很好的方法。

此外,你需要通过阅读ANTLR 生成的解析器源码、所使用的ANTLR 运行时的源码以及相关的文献,深入理解 ANTLR 所基于的分析技术和实现策略。比较它与常规的 LL分析方法的区别,总结它对分析中遇到的试探策略以及试探失败后的处理机制。将你的理解和相应的验证性例子等写到本实验的工作报告中。

重点与难点

在本次实验中,你可以阅读 对ANTLR生成的代码进行编程 来起步,你需要:

  • 了解、使用ANTLR分析树的编程接口,书面总结它们与你在lab1-2中写的文法之间的关系;
  • 学习使用 Vistor 访问者模式来构建AST;

  • 深入理解ANTLR的分析原理,结合生成的解析器源码以及所调用的ANTLR运行时的实现代码,调研相关的文献,回答:

    • ATN的英文全称是什么,它代表什么?
    • SLL的英文全称是什么,它和LL、以及ALL(*)的区别是什么? 它们分别怎么对待文法二义、试探回溯?
    • 了解并总结enterRecursionRule、unrollRecursionContexts、adaptivePredict函数的作用、接口和主要处理流程。
    • 错误处理机制

提交说明

你需要在 Lab1-2 的基础上修改并组织你本次实验的提交文件,具体如下:

- <your repo>
  | c1recognizer         复制自公共仓库的 c1recognizer 项目。请勿遗漏内容。
    | cmake/
    | grammar/           
    | include/c1recognizer/ 
>>  | src/                 你需要在其中补充你的实现代码
>>  | test/test_cases/   如果有更多的测试程序,可以在其中补充
>>  | doc/               你需要在其中增加文档描述你在实验中遇到的问题、你的分析和设计。你还需要在其中对"重点与难点"小节中的问题进行回答。
    | 其他已有的文件

ANTLR v4 的使用

ANTLR v4 是一个基于 LL(*) 分析方法 [PLDI 2011] 、Adaptive LL(*)分析[OOPSLA 2014]的LL解析器的生成工具(Parser Generator)。通过输入一个或多个.g4格式的文法描述文件,它能够生成一套解析器的源程序。生成的解析器依赖于 ANTLR v4 的运行时库,能够执行指定的文法分析过程,并可以通过 ANTLR v4 运行时提供的接口和数据格式来调用与访问。

ANTLR v4 所提倡的思想在于,使用文法描述文件描述语言本身,而与实现无关。也就是说,文法描述文件应当只是对语言本身文法的一个规范描述,它不应包括其实现逻辑(虽然 ANTLR 具备在文法描述文件中附加程序代码的功能,但使用它是不被提倡的;这一功能是从 ANTLR v3 遗留下来的);直到根据输入生成 parse tree 后,才通过用 ANTLR 工作语言编写的 Visitor 遍历parse tree 并生成 AST。

文法描述文件(.g4)的编写

这一格式的作用是描述语言的文法。其中能够定义终结符和非终结符,终结符定义词法记号,非终结符定义语法结构。

ANTLR v4 允许在单个文法描述文件中描述语言的词法和语法,但是本实验会将词法描述和语法描述分离成多个文件。分离的原因主要是为了便于分步进行课程实验、调试和提交,也有助于理解和评分。

在用于本课程实验的初始代码中,给出了全部需要完成的终结符和非终结符,但是只填入了能够完成简单的整数加、减、乘、除、取余表达式解析的文法描述。你需要在本实验中将它完善至能够解析完整的 C1 语言。

词法分析(Lexer)

词法分析的文法文件(未完成)位于课程实验软件包的c1recognizer/src/grammar/C1Lexer.g4。它的初始内容如下:

lexer grammar C1Lexer;

tokens {
    Comma,
    SemiColon,
    Assign,
    LeftBracket,
    RightBracket,
    LeftBrace,
    RightBrace,
    LeftParen,
    RightParen,
    If,
    Else,
    While,
    Const,
    Equal,
    NonEqual,
    Less,
    Greater,
    LessEqual,
    GreaterEqual,
    Plus,
    Minus,
    Multiply,
    Divide,
    Modulo,

    Int,
    Void,

    Identifier,
    Number
}

LeftParen: '(';
RightParen: ')';

Plus: '+' ;
Minus: '-' ;
Multiply: '*' ;
Divide: '/' ;
Modulo: '%' ;

Number: [0-9]+ | '0x' [0-9a-fA-F]+ ;

WhiteSpace: [ \t\r\n]+ -> skip;
文法类型和分析器名

在ANTLR 的文法文件格式中,首先应注明文法类型和分析器名:lexer grammar C1Lexer;表示该文法文件描述了一个词法分析器,其名称为 C1Lexer。

记号声明

tokens{...}中包含的是所有的 token 名,以逗号分隔。这里已经列出了你会用到的所有 token。需要注意的是:

  • 在 ANTLR 语法文件中,token 名需以大写字母开头;
  • 空白符等不包含在其中。

你可以看到,在文件最后一行出现的 WhiteSpace 并不在这里出现,这是因为它们所代表的文本将被忽略(通过命令 -> skip 来表达,后面会有详细解释),不会出现在词法分析器 C1Lexer 输出的 token 流(ANTLR 运行时库中的TokenStream)中。

文法规则——词法规则

接下来则是文法规则。在lexer grammar中,这里出现的规则必须是对词法单元的描述,即对 token 的定义。每条规则由大写字母开头的 token 名开始,后跟一个冒号,然后是对该 token 构成规则的描述。一个 token 可以由多种模式匹配到,各个模式之间以|分隔,规则以;结束。

规则的表示

规则的表述与正则表达式大致相同。例如,你可以看到'blabla'表示相应的字面文本,[0-9] 表示相应的单字符可能值,+ 作为后缀修饰符表示一次或多次出现相应的文本;此外,还有?作为后缀修饰符表示被修饰文本的可选出现,而 (somerule)* 等价于 ((somerule)+)?。这些都与正则表达式的表示相同。但是需要注意的是,不同点也是存在的:在课程实验中主要会涉及的不同点在于,正则表达式中使用 [^a-z] 表示一个非小写字母的字符,但是在 ANTLR 中使用记号 ~[a-z],其中 ~ 为一个前缀修饰符,用于匹配一个不在此修饰符后描述的字符范围内的字符;这里的“范围”可能不仅由[] 内的规则指定,还可能是单字符的字面值,例如 ~'a'表示一个非a的字符。

规则的表述与正则表达式大致相同。例如你可以看到'blabla'表示相应的字面文本,[0-9]表示相应的单字符可能值,+作为后缀修饰符表示一次或多次出现相应的文本;此外,还有?作为后缀修饰符表示被修饰文本可选出现,而(somerule)*等价于((somerule)+)?。这些都与正则表达式的特性相同。但需要注意的是,不同点也是存在的:我们主要会涉及到的不同点在于,正则表达式中使用[^a-z]表示一个非小写字母的字符,但 ANTLR 中使用记号~[a-z],其中~为一个前缀修饰符,用于匹配一个不在此修饰符后描述的字符范围内的字符;这里“范围”可能不仅由[]内的规则指定,还可能是单字符的字面值,例如~'a'表示一个非a的字符。

本次实验中匹配注释是最困难的部分。你可能会用到一些后缀标识符,如*?,它代表 non-greedy many,也就是说在此处匹配尽可能少的内容,使整条规则得到匹配。其它的简单后缀标识符,包括+?,都有它们的 non-greedy 版本,即+???,它们的含义也类似。

命令

每一条规则结尾处可附加一条命令,跟随在 -> 之后。本次实验中仅会用到命令 skip,它表示此条规则匹配到的 token 会被忽略,不计入 TokenStream中。

其他

此外,ANTLR 允许在词法规则中附加操作代码,也允许词法规则中出现嵌套和递归,同时还有匹配模式(mode)的切换和模式栈机制的存在。但本次实验中不会涉及这些内容,单一模式已经能够支持所需的处理(C1 语言中没有字符串字面值的存在)。如果你想了解更多关于 ANTLR Lexer 规则的信息,请参考 Lexer Rules

语法分析(Parser)

语法分析的语法文件(未完成)位于c1recognizer/src/grammar/C1Parser.g4。它的初始内容如下:

parser grammar C1Parser;
options { tokenVocab = C1Lexer; }

compilationUnit: ;
decl: ;
constdecl: ;
constdef: ;
vardecl: ;
vardef: ;
funcdef: ;
block: ;
stmt: ;
lval: ;
cond: ;
exp:
    (Plus | Minus) exp
    | exp (Multiply | Divide | Modulo) exp
    | exp (Plus | Minus) exp
    | LeftParen exp RightParen
    | Number
;

这里的语法文件类型为parser grammar,名称为C1Parser

options { tokenVocab = C1Lexer; }表示从C1Lexer引入 token 字典,这里不需要更改。

非终结符名称需要以小写字母开头;每条规则由非终结符名称开始,冒号后跟随多条可选规则,用|分隔。规则可以出现左递归,但出现左递归的标识符必须存在非左递归的匹配规则;各种后缀标识符(? * + ?? *? +?)含义与 Lexer 阶段相同,不再赘述。

出现左递归的情形下,运算符优先级按从上到下依次降低。默认的结合性为左结合,我们的 C1 语言中不涉及双目以上右结合的运算,因此你暂时无须理会结合性的问题。

如果你想了解更多关于 ANTLR Parser 规则的信息,请参考 Parser Rules

可以看到,我们的语法分析文件中没有任何代码段,这贯彻了 ANTLR v4 的设计思想。下一小节中会介绍如何使用分析结果。

测试语法文件并可视化

ANTLR v4 具备独立测试语法文件、将分析图和分析结果可视化的功能。

为可视化分析图,推荐使用 Visual Studio Code 作为编辑器,安装 ANTLR 插件。在一个.g4文件的编辑窗口中右键,你会看到几个选项,点击即可。

为测试语法文件并可视化分析结果,你首先需要按照 Environment 页面中的要求配置好 ANTLR v4 的工作环境。在此前提下,按如下步骤完成测试(在src/grammar目录中):

# Compile grammar to Java source code
antlr4 *.g4
# Compile Java source code
javac *.java
# Testing lexer
grun C1Lexer tokens -tokens ../test/test_cases/simple.c1
# Testing lexer + parser, GUI version parse tree
grun C1 compilationUnit -gui ../test/test_cases/simple.c1
# Testing lexer + parser, console printed version parse tree
grun C1 compilationUnit -tree ../test/test_cases/simple.c1

这里simple.c1是预置的简易测试文件。由于我们最初提供的代码中仅包含了exp的分析,你需要将compilationUnit更换为exp,并用你自己建立的表达式输入样例取代../test/test_cases/simple.c1作为输入文件,才能正确查看结果。当然,这里的测试文件你可以任意选择,确认好文件位置就好。

grun 的命令中你也可以不指定输入文件,采用标准输入流作为输入文本。这时候你需要键入一个 EOF 标志来结束输入:在 Linux 等环境中是 Ctrl-D,在 Windows 下则是在空行中 Ctrl-Z 并回车。

对ANTLR生成的代码进行编程

在代码中使用 ANTLR 生成的程序是较为麻烦的一件事。但我们已经构建好了一套基于 CMake 的自动构建过程(后面会进行介绍),并准备好了基本的程序,你只需要了解 Parse Tree 的结构、以及基于访问者模式编写构建AST的代码方法即可。

1)Parse Tree的结构

以 C1Parser.g4 生成的C1Parser.cpp为例,我们这里截取部分代码:

namespace c1_recognizer {


class  C1Parser : public antlr4::Parser {
public:
  enum {
    LeftParen = 1, RightParen = 2, Plus = 3, Minus = 4, Multiply = 5, Divide = 6, 
    Modulo = 7, Number = 8, WhiteSpace = 9
  };

  enum {
    RuleCompilationUnit = 0, RuleDecl = 1, RuleConstdecl = 2, RuleConstdef = 3, 
    RuleVardecl = 4, RuleVardef = 5, RuleFuncdef = 6, RuleBlock = 7, RuleStmt = 8, 
    RuleLval = 9, RuleCond = 10, RuleExp = 11
  };

  C1Parser(antlr4::TokenStream *input);
  ~C1Parser();

  ...

  class ExpContext; 

  ...

  class  ExpContext : public antlr4::ParserRuleContext {
  public:
    ExpContext(antlr4::ParserRuleContext *parent, size_t invokingState);
    virtual size_t getRuleIndex() const override;
    std::vector<ExpContext *> exp();
    ExpContext* exp(size_t i);
    antlr4::tree::TerminalNode *Plus();
    antlr4::tree::TerminalNode *Minus();
    antlr4::tree::TerminalNode *LeftParen();
    antlr4::tree::TerminalNode *RightParen();
    antlr4::tree::TerminalNode *Number();
    antlr4::tree::TerminalNode *Multiply();
    antlr4::tree::TerminalNode *Divide();
    antlr4::tree::TerminalNode *Modulo();

    virtual void enterRule(antlr4::tree::ParseTreeListener *listener) override;
    virtual void exitRule(antlr4::tree::ParseTreeListener *listener) override;

    virtual antlrcpp::Any accept(antlr4::tree::ParseTreeVisitor *visitor) override;

  };

  ExpContext* exp();
};

}  // namespace c1_recognizer

这当中,ExpContext是 Parse Tree 中代表exp非终结符的节点;其它的命名中包含Context的类也类似。

其成员中,std::vector<ExpContext *> exp();ExpContext* exp(size_t i);返回的是这一节点的孩子中代表exp非终结符的节点,前者返回全部,后者返回第i个。生成这些接口的原因在于,产生式规则中可能存在多个exp非终结符,作为当前节点的孩子;如果最多只有一个,生成的接口会形如ExpContext* exp();,返回唯一一个这样的孩子节点(如果实际上并不存在,则会返回nullptr,即 C 语言中的NULL)。ExpContext中的其它诸如antlr4::tree::TerminalNode *Plus();的接口,返回的是相应名称的终结符节点,若不存在,返回的则是nullptr

如果你需要从一个TerminalNode指针p获取其匹配的原文文本,你应使用p->getSymbol()->getText()。这在你处理IdentifierNumber时会有用(但是Number已经处理了)。

2)AST及其构建

为了从Parse Tree生成 AST(我们已经准备好了完整的数据结构,位于syntax_tree.h中,请阅读之,尤其是其中的注释)。

你需要在 syntax_tree_builder.cpp 中补充代码,以实现对AST的构建。在 syntax_tree_builder.cpp 已提供的初始代码中,你需要对其中形如visitSomeContext的函数补充相应的实现代码。请在你的代码编辑器中查看 syntax_tree_builder.cpp ,理解:

在理解之后,分析C1的Parse Tree结构,实现通过访问Parse Tree来构建AST。

错误处理与恢复

ANTLR 已经有相当丰富的默认错误恢复过程。有兴趣的同学可以阅读build/externals/antlr4cpp/src/antlr4cpp/runtime/Cpp/runtime/src/DefaultErrorStrategy.cpp中的实现;其中的接口功能参照build/externals/antlr4cpp/src/antlr4cpp/runtime/Cpp/runtime/src/DefaultErrorStrategy.hbuild/externals/antlr4cpp/src/antlr4cpp/runtime/Cpp/runtime/src/ANTLRErrorStrategy.h

这一默认错误恢复逻辑可以处理单 token 的冗余和缺失,在失配时自动扫描至下一处可以开始匹配的位置,已经较为智能。但要让它使一些语法错误以 Warning 的形式通过编译是不现实、不优雅的,因此我们本次实验的错误处理主要围绕 Warning 进行。

你需要使分析器能够处理const i = 0;这样的代码,并为i默认类型int。此处的默认类型指定对数组声明也是同样。

为此,你需要修改文法,使这样的输入能在不失配的情况下构建好 parse tree;也就是说,我们在文法层承认这样的输入的合法性。但在此后的 AST 生成中,如果发现 token int缺失,你需要报出一个 warning。

syntax_tree_builder在构造时传入了错误报告对象err,并将其保存在自身成员中,你可以在syntax_tree_builder::visitSomeContext中直接使用它来输出一个 warning:

err.warn(token->getLine(), token->getCharPositionInLine(), "message here");

TerminalNode *p,通过p->getSymbol()可以获取其token。报错位置应在const处。

实验代码的编译和运行

如果你希望为编译过程指定 C++ 编译器,请修改环境变量 CXX 为你希望使用的编译器。CMake 会自动处理。

编译过程分两步:

  1. 使用 CMake 创建 Makefile

    mkdir -p build
    cd build
    cmake -DCMAKE_BUILD_TYPE=Debug -DANTLR4CPP_JAR_LOCATION=<path-to-antlr>/antlr-4.7-complete.jar ..
    make
    cmake ..
    

    如此,你便在build目录下创建了用于编译实验代码的 Makefile。注意,请不要使用build以外的文件夹名,否则会污染 Git 仓库。

    之所以使用 Debug 配置,是为了方便你进行调试。如果希望测试性能(但是我们不会依照性能评分,安心して)你可以将 Debug 改为 Release,CMake 会自动开启 -O2 优化。

    这里进行的一次 make 操作会下载 antlrcpp,并对其进行编译。时间会较长,请安心等待。这里不能并行编译,原因在于 CMake 对于外部库的依赖解析有一些问题。此次编译必定会失败,但编译完成后,重新运行 CMake,即可生成正确的 Makefile。这是由于 antlr4cpp 的 CMake 脚本的实现方式错误导致的,会稍微麻烦一点,不过像我们这样重新生成 Makefile 就能够解决,算是一个 workaround。

    这一步骤只需要执行一次。项目中的build.sh即是执行这一步骤后进行编译;如果你打算使用build.sh进行第一次编译,你需要修改其中的变量。你也可以不使用build.sh

  2. 进行编译

    build目录中执行

    make -j
    

编译后,build目录下会出现可执行程序c1r_test。这一程序的功能是根据命令行参数中的文件名,读取其内容作为源代码输入,经过你的 C1LexerC1Parsersyntax_tree_builder,在标准输出中以 JSON 格式序列化输出 AST。它已经实现好,你可以直接用于测试。例如在build目录下如下使用:

./c1r_test ../test/test_cases/simple.c1

对于初始的表达式解析器,这一测试程序也是可以工作的。将simple.c1换成相应的输入文件,例如我们提供好的../../test/test_cases/exp_example,即可。推荐你在拿到代码的第一时间进行一次编译,并尝试一下解析这一示例表达式输入。

需要注意的是,这里解析的起始符号是在recognizer.cpp中的第 44 行代码指定的;如果要更换,你需要修改这里的代码,例如,最终提交时你必须将其修改为compilationUnit

关于构建系统的解释

本次实验的代码采用了 CMake 作为构建系统,其主要行为在 CMakeLists.txt 中指定。

在其中,ExternalAntlr4Cpp.cmake被引用、用以将 ANTLR 的语法文件的代码生成集成进构建过程中;另外,还利用ExternalProject_Add宏引用了rapidjson库,用以在c1r_test中完成 AST 的序列化输出。

然后,它添加了c1recognizer库和c1r_test可执行程序项目,并配置了安装步骤。安装步骤的存在是为了支持下一阶段的实验;我们将可以直接引用安装好的c1recognizer,这也是 CMake 推荐的项目依赖方式。

results matching ""

    No results matching ""