diff --git a/docs/.latexmkrc b/docs/.latexmkrc new file mode 100644 index 0000000..b5875ee --- /dev/null +++ b/docs/.latexmkrc @@ -0,0 +1,7 @@ +$pdf_mode = 1; +$pdflatex = "xelatex -file-line-error --shell-escape -src-specials -synctex=1 -interaction=nonstopmode %O %S;cp %D %R.pdf"; +$recorder = 1; +$clean_ext = "synctex.gz acn acr alg aux bbl bcf blg brf fdb_latexmk glg glo gls idx ilg ind ist lof log lot out run.xml toc dvi"; +$bibtex_use = 2; +$out_dir = "temp"; +$jobname = "CanonReport"; \ No newline at end of file diff --git a/docs/assets/DFA状态闭包流程图.png b/docs/assets/DFA状态闭包流程图.png new file mode 100644 index 0000000..3f337b9 Binary files /dev/null and b/docs/assets/DFA状态闭包流程图.png differ diff --git a/docs/assets/FIRST流程图.png b/docs/assets/FIRST流程图.png new file mode 100644 index 0000000..1dcfecb Binary files /dev/null and b/docs/assets/FIRST流程图.png differ diff --git a/docs/assets/代码生成/赋值语句的翻译.png b/docs/assets/代码生成/赋值语句的翻译.png new file mode 100644 index 0000000..e4abb1f Binary files /dev/null and b/docs/assets/代码生成/赋值语句的翻译.png differ diff --git a/docs/assets/持续测试示例.png b/docs/assets/持续测试示例.png new file mode 100644 index 0000000..cd85b69 Binary files /dev/null and b/docs/assets/持续测试示例.png differ diff --git a/docs/assets/数据流图.png b/docs/assets/数据流图.png new file mode 100644 index 0000000..b3ca2d9 Binary files /dev/null and b/docs/assets/数据流图.png differ diff --git a/docs/assets/测试/(n+n)n语法树.jpg b/docs/assets/测试/(n+n)n语法树.jpg new file mode 100644 index 0000000..2f4e0b6 Binary files /dev/null and b/docs/assets/测试/(n+n)n语法树.jpg differ diff --git a/docs/assets/测试/aa语法树.jpg b/docs/assets/测试/aa语法树.jpg new file mode 100644 index 0000000..432cd0a Binary files /dev/null and b/docs/assets/测试/aa语法树.jpg differ diff --git a/docs/assets/测试/intgration_test.jpg b/docs/assets/测试/intgration_test.jpg new file mode 100644 index 0000000..d3f1984 Binary files /dev/null and b/docs/assets/测试/intgration_test.jpg differ diff --git a/docs/assets/测试/n+n语法树.jpg b/docs/assets/测试/n+n语法树.jpg new file mode 100644 index 0000000..2a8d431 Binary files /dev/null and b/docs/assets/测试/n+n语法树.jpg differ diff --git a/docs/assets/测试/opentestfailed.jpg b/docs/assets/测试/opentestfailed.jpg new file mode 100644 index 0000000..4ee4224 Binary files /dev/null and b/docs/assets/测试/opentestfailed.jpg differ diff --git a/docs/assets/测试/test.jpg b/docs/assets/测试/test.jpg new file mode 100644 index 0000000..1d26c88 Binary files /dev/null and b/docs/assets/测试/test.jpg differ diff --git a/docs/assets/测试/test_fail.jpg b/docs/assets/测试/test_fail.jpg new file mode 100644 index 0000000..d6f051c Binary files /dev/null and b/docs/assets/测试/test_fail.jpg differ diff --git a/docs/assets/测试/测试程序.png b/docs/assets/测试/测试程序.png new file mode 100644 index 0000000..177fe42 Binary files /dev/null and b/docs/assets/测试/测试程序.png differ diff --git a/docs/assets/示例语法树图.png b/docs/assets/示例语法树图.png new file mode 100644 index 0000000..29a6c70 Binary files /dev/null and b/docs/assets/示例语法树图.png differ diff --git a/docs/assets/类型检查/Expression.png b/docs/assets/类型检查/Expression.png new file mode 100644 index 0000000..3f62936 Binary files /dev/null and b/docs/assets/类型检查/Expression.png differ diff --git a/docs/assets/类型检查/Factor.png b/docs/assets/类型检查/Factor.png new file mode 100644 index 0000000..fd9b872 Binary files /dev/null and b/docs/assets/类型检查/Factor.png differ diff --git a/docs/assets/类型检查/IdVarPart.png b/docs/assets/类型检查/IdVarPart.png new file mode 100644 index 0000000..22ac50d Binary files /dev/null and b/docs/assets/类型检查/IdVarPart.png differ diff --git a/docs/assets/类型检查/IdentifierList.png b/docs/assets/类型检查/IdentifierList.png new file mode 100644 index 0000000..d195976 Binary files /dev/null and b/docs/assets/类型检查/IdentifierList.png differ diff --git a/docs/assets/类型检查/SimpleExpression.png b/docs/assets/类型检查/SimpleExpression.png new file mode 100644 index 0000000..ca9ae6f Binary files /dev/null and b/docs/assets/类型检查/SimpleExpression.png differ diff --git a/docs/assets/类型检查/Subprogram.png b/docs/assets/类型检查/Subprogram.png new file mode 100644 index 0000000..79dbd75 Binary files /dev/null and b/docs/assets/类型检查/Subprogram.png differ diff --git a/docs/assets/类型检查/SubprogramHead.png b/docs/assets/类型检查/SubprogramHead.png new file mode 100644 index 0000000..e251ce0 Binary files /dev/null and b/docs/assets/类型检查/SubprogramHead.png differ diff --git a/docs/assets/类型检查/Term.png b/docs/assets/类型检查/Term.png new file mode 100644 index 0000000..00a6034 Binary files /dev/null and b/docs/assets/类型检查/Term.png differ diff --git a/docs/assets/类型检查/TypeSyntaxNode.png b/docs/assets/类型检查/TypeSyntaxNode.png new file mode 100644 index 0000000..2fddeb1 Binary files /dev/null and b/docs/assets/类型检查/TypeSyntaxNode.png differ diff --git a/docs/assets/类型检查/constDeclaration.png b/docs/assets/类型检查/constDeclaration.png new file mode 100644 index 0000000..e9bfddb Binary files /dev/null and b/docs/assets/类型检查/constDeclaration.png differ diff --git a/docs/assets/类型系统.png b/docs/assets/类型系统.png new file mode 100644 index 0000000..64778f0 Binary files /dev/null and b/docs/assets/类型系统.png differ diff --git a/docs/assets/编译器Web在线版本.png b/docs/assets/编译器Web在线版本.png new file mode 100644 index 0000000..5a04da6 Binary files /dev/null and b/docs/assets/编译器Web在线版本.png differ diff --git a/docs/assets/编译器Web在线版本_历史记录.png b/docs/assets/编译器Web在线版本_历史记录.png new file mode 100644 index 0000000..5ba6355 Binary files /dev/null and b/docs/assets/编译器Web在线版本_历史记录.png differ diff --git a/docs/assets/编译器Web在线版本_语法树.png b/docs/assets/编译器Web在线版本_语法树.png new file mode 100644 index 0000000..24f3e83 Binary files /dev/null and b/docs/assets/编译器Web在线版本_语法树.png differ diff --git a/docs/assets/集成测试示例.png b/docs/assets/集成测试示例.png new file mode 100644 index 0000000..94a7d2e Binary files /dev/null and b/docs/assets/集成测试示例.png differ diff --git a/docs/assets/项目集闭包流程图.png b/docs/assets/项目集闭包流程图.png new file mode 100644 index 0000000..e25ae51 Binary files /dev/null and b/docs/assets/项目集闭包流程图.png differ diff --git a/docs/contents/assets/1.jpg b/docs/contents/assets/1.jpg new file mode 100644 index 0000000..2f6d36f Binary files /dev/null and b/docs/contents/assets/1.jpg differ diff --git a/docs/contents/detailed-design.tex b/docs/contents/detailed-design.tex new file mode 100644 index 0000000..33f6a67 --- /dev/null +++ b/docs/contents/detailed-design.tex @@ -0,0 +1,1459 @@ +\documentclass[../main.tex]{subfiles} + +\begin{document} +\section{详细设计} + +% 说明,包括: +% 接口描述 +% 功能描述 +% 所用数据结构说明 +% 算法描述 + +% main4.tex 暂时更完了,最后再往里面加图————话说你们有矢量图(指word里面的那些图)嘛,没有的话我看怎么自己处理一下 + +% 一个图片引用的示例 +% \begin{figure}[ht] +% \centering +% \includegraphics[width=\linewidth]{assets/1.jpg} +% \caption{非终结符的First集合计算示例} +% \label{fig:first_set} +% \end{figure} + +% 备注一下,本部分的图片等资源暂未上传 + +\subsection{词法分析} + +词法分析器由以下类构成: +\begin{itemize} +\item 词法分析器主体 - Lexer + \begin{itemize} + \item 保存词法元素(关键字、分隔符等) + \item 给予词法状态机输入,将获得的Token添加到输出列表 + \end{itemize} +\item 构造记号的工厂 - TokenFactory + \begin{itemize} + \item 构造并返回不同的记号类 + \end{itemize} +\item 翻译表产生的判断规则 - LexRules + \begin{itemize} + \item 提供判断字符类型的方法 + \end{itemize} +\end{itemize} + +\subsubsection{功能} + +对于一些能在词法分析阶段完成的工作,将尽可能提前完成,如: +\begin{itemize} +\item 对十六进制数字的字面量进行修改,将“\$”修改为C风格的“0x”; +\item 判断数值类型,划分为Real,Hex,Integer; +\item 对“.”符号进行细化,区分为Dot和Period; +\item 避免大小写对后阶段分析的干扰,将关键字等类别转化为Enum类型; +\end{itemize} + +此外,对注释可以识别以下三种: +\begin{itemize} +\item {……} +\item (*……*) +\item // +\end{itemize} + +\texttt{Lexer} 是接口\texttt{ILexer}的实现类,其的功能包括: +\begin{itemize} + \item 注释处理:识别并跳过源代码中的注释。 + \item 词法记号生成:根据源代码生成相应的词法记号,包括关键字、标识符、数值、运算符、分隔符等。 + \item 错误处理:在源代码中识别非法字符和格式错误,并抛出错误信息。 +\end{itemize} + +\subsubsection{数据结构及内部算法} + +\paragraph{LexemeFactory} +采用工厂模式,提供了静态的多态的方法 \texttt{MakeToken} 来创建不同类型的词法记号。 + +\begin{lstlisting}[ + style=csharp +] +public static SemanticToken MakeToken(SemanticTokenType tokenType, + string literal, uint _line, uint _chPos) +{ + /* 实际的逻辑代码 */ +} +\end{lstlisting} + +\paragraph{LexRules} +\texttt{LexRules} 包含了各种词法规则的静态方法,用于判断字符是否为字母、数字、运算符等。此外,它还为关键字、分隔符和操作符提供了字符串数组来记录内容,这些内容可根据语法分析进行调整或进一步拓展。这些方法非常重要,因为它们确保词法分析器可以正确地识别各种词法元素,从而为语法分析阶段准备正确的输入。该类的中的函数有: +\begin{itemize} + \item \textbf{IsKeyword}: 判断一个字符串是否为预定义的关键字。这有助于词法分析器正确区分标识符和保留关键字。它能将大小写字母看作同一字符进行比较,确保Pascal转C语言的语法正确性。 + \item \textbf{IsDelimiter}: 判断字符是否为词法单元的分隔符。 + \item \textbf{IsOperator}: 判断字符是否为运算符,支持词法分析的运算符识别。 + \item \textbf{IsDigit}: 判断字符是否为数字,用于识别数值。 + \item \textbf{IsHexDigit}: 判断字符是否为十六进制数的有效字符。 + \item \textbf{IsLetter}: 判断字符是否为字母或下划线,这对于识别标识符尤其重要。 +\end{itemize} + +\paragraph{Lexer} 词法分析器\texttt{Lexer}的核心是一个以状态机为基础的类,用于根据输入文本的不同部分切换状态,识别和生成不同的词法记号。以下是状态机可能进入的状态,使用枚举类型实现: + +\begin{lstlisting}[ + style=csharp +] +public enum StateType +{ + Start, // 初始状态,用于开始分析新的token + Comment, // 处理注释的状态 + Word, // 处理单词(关键字或标识符) + Num, // 处理数字(整数或浮点数) + Delimiter, // 处理分隔符 + Operator, // 处理操作符 + Unknown, // 遇到未知字符或字符串 + Done // 完成所有分析 +} +\end{lstlisting} + +\texttt{Tokenize()} 函数是状态机的主要工作函数,负责根据当前状态来处理输入的源代码字符串,并进行状态转移,直到读取完整个字符串。最终,它将返回一个词法记号流 (\texttt{\_tokens})。词法记号的字面量通过 \texttt{StringBuilder} 类记录。 + +\begin{lstlisting}[ + style=csharp +] +public List Tokenize() +{ + while (_state != StateType.Done) // 当状态不是“完成”时,继续处理输入 + { + switch (_state) + { + case StateType.Start: + HandleStartState(); // 处理起始状态 + break; + case StateType.Comment: + // 根据注释的类型调用不同的处理函数 + if (_ch == '{') + { + HandleCommentStateBig(); + } + else if (_ch == '*') + { + HandleCommentStateSmall(); + } + else + { + HandleCommentSingleLine(); + } + break; + case StateType.Num: + HandleNumState(); // 处理数值状态 + break; + case StateType.Word: + HandleWordState(); // 处理单词状态 + break; + case StateType.Delimiter: + HandleDelimiterState(); // 处理界符状态 + break; + case StateType.Operator: + HandleOperatorState(); // 处理操作符状态 + break; + case StateType.Unknown: + // 抛出未知词法元素异常 + throw new LexemeException(LexemeErrorType.UnknownCharacterOrString, _line, _chPos, "Illegal lexeme."); + case StateType.Done: + break; // 完成所有处理,退出循环 + } + } + return _tokens; // 返回生成的词法记号流 +} +\end{lstlisting} + +\texttt{HandleStartState()} 函数是词法分析器中首个调用的函数,负责从源代码中读取字符并确定其对应的状态。函数首先检查是否达到输入字符串的结尾,然后根据字符决定下一状态。此函数是实现词法分析的状态机逻辑的关键入口点,其设计考虑了各种字符输入可能性并做出相应状态转换。以下是该函数的详细解释与实现: + +\begin{lstlisting}[ + style=csharp +] +private void HandleStartState() +{ + ResetTokenBuilder(); // 重置token构建器,准备构建新的token + + GetChar(); // 读取下一个字符 + GetNbc(); // 忽略空白字符 + if (_finish) // 检查是否已经处理完所有输入 + { + _state = StateType.Done; // 设置状态为完成 + return; + } + + // 根据读取的字符,判断进入哪个状态 + if (_ch == '{') // 处理以 "{" 开始的注释 + { + _state = StateType.Comment; + } + else if (_ch == '(') // 处理以 "(" 开始可能是注释的情况 + { + char nextChar = PeekNextChar(); + if (nextChar == '*') + { + GetChar(); // 消费 "*" + _state = StateType.Comment; + } + else + { + _state = StateType.Delimiter; + } + } + else if (_ch == '/') // 处理以 "/" 开始可能是注释或运算符的情况 + { + char nextChar = PeekNextChar(); + if (nextChar == '/') + { + GetChar(); // 消费第二个 "/" + _state = StateType.Comment; + } + else + { + _state = StateType.Operator; + } + } + else if (_ch == '.') // 处理点号,可能是小数点或分隔符 + { + char next = PeekNextChar(); + if (next >= '0' && next <= '9') // 是小数部分,进入数字状态 + { + _state = StateType.Num; + } + else // 不是小数点,是普通分隔符 + { + _state = StateType.Delimiter; + } + } + else if (LexRules.IsLetter(_ch)) // 字母开头,可能是关键字或标识符 + { + _state = StateType.Word; + } + else if (LexRules.IsDigit(_ch) || _ch == '$') // 数字或 "$" 开头,为数值 + { + _state = StateType.Num; + } + else if (LexRules.IsDelimiter(_ch)) // 分隔符 + { + _state = StateType.Delimiter; + } + else if (LexRules.IsOperator(_ch)) // 运算符 + { + _state = StateType.Operator; + } + else // 未知字符 + { + _state = StateType.Unknown; + } +} +\end{lstlisting} + +\textit{注:上述代码段中的每一步操作都对应于词法分析中可能的各种字符类别,从而决定了后续的状态转换。例如,处理注释、运算符、关键字等,都有对应的逻辑处理分支,确保了语法分析的准确性和高效性。} + +处理标识符和关键字的状态函数\texttt{HandleWordState()}负责区分输入字符序列是关键字还是普通的标识符。利用 \texttt{LexRules} 类来确定是否为关键字,并据此生成相应的词法记号。 + +\begin{lstlisting}[ + style=csharp +] +private void HandleWordState() +{ + // 继续读取字符直到遇到非字母或数字为止 + while (LexRules.IsDigit(_ch) || LexRules.IsLetter(_ch)) + { + Cat(); // 追加当前字符到token + GetChar(); // 读取下一个字符 + } + + Retract(); // 回退一个字符 + string tokenString = GetCurrentTokenString(); // 获取当前token字符串 + + // 判断是否为关键字 + if (LexRules.IsKeyword(tokenString)) + { + KeywordType keywordType = KeywordSemanticToken.GetKeywordTypeByKeyword(tokenString); + _semanticToken = LexemeFactory.MakeToken(keywordType, tokenString, _line, _chPos); + } + else // 否则作为普通标识符处理 + { + _semanticToken = LexemeFactory.MakeToken(SemanticTokenType.Identifier, tokenString, _line, _chPos); + } + + AddToTokens(_semanticToken); // 添加token到列表 + _state = StateType.Start; // 返回到起始状态 +} +\end{lstlisting} + + +处理界符状态函数\texttt{HandleDelimiterState()}处理特定的界符,包括单双引号内的字符序列。 + +\begin{lstlisting}[ + style=csharp +] +private void HandleDelimiterState() +{ + Cat(); + switch (_ch) + { + /* 省略实际的逻辑代码 */ + } + + if (_semanticToken is null) + { + throw new InvalidOperationException(); + } + _tokens.Add(_semanticToken); + _state = StateType.Start; +} +\end{lstlisting} + +处理操作符状态函数\texttt{HandleOperatorState()}类似于处理界符的函数,此函数专门处理各类操作符。 + +\begin{lstlisting}[ + style=csharp +] +private void HandleOperatorState() +{ + // 识别并处理不同的操作符 + switch (_ch) + { + case '+': + _semanticToken = LexemeFactory.MakeToken(OperatorType.Plus, "+", _line, _chPos); + break; + case '-': + _semanticToken = LexemeFactory.MakeToken(OperatorType.Minus, "-", _line, _chPos); + break; + case '*': + _semanticToken = LexemeFactory.MakeToken(OperatorType.Multiply, "*", _line, _chPos); + break; + case '/': + _semanticToken = LexemeFactory.MakeToken(OperatorType.Divide, "/", _line, _chPos); + break; + default: + ProcessComplexOperators(); // 处理更复杂的操作符 + break; + } + AddToTokens(_semanticToken); + _state = StateType.Start; +} +\end{lstlisting} + +\subsection{语法分析} + +\subsubsection{概述} + +语法分析是编译过程中的核心部分,涉及从源代码中提取语法结构并构建语法树。在LR(1)分析中,我们通过构建项目集规范族来处理文法中的前瞻符号,从而有效地解析各种语法构造。 + +语法分析模块首先通过给定的语法构建LR的自动机,再通过自动机构建LR(1)的分析表。得到分析表之后再按照分析表对输入的词法记号流进行分析并得到语法树。 + +其中鉴于从语法构建自动机再得到分析表是一个时间复杂度极高的过程,而且在语法给定之后的分析表不会发生变化,不需要在每次编译的过程中重新生成分析表。因此需要编写一个直接将分析表转化为源代码的工具,直接将分析表编译到编译器中去。 + +语法分析模块输出的结果为一棵语法树。语法树上的节点可以分成非终结符节点和终结符节点,其中非终结符节点对应语法中的非终结符,是语法树上的父节点,其子节点可以是非终结符节点和终结符节点;而终结符节点直接对应词法分析得到的记号流中的一个记号,从该节点上可以直接获得该记号。 + +因此将语法树节点分成两类分别实现。其中非终结符节点直接对应按照非终结符的类型各个实现,方便后续将类型检查和目标代码生成的逻辑封装在各个非终结符节点类型中。而终结符节点只有一种类型,提供获得对应词法记号的方法。 + +\subsubsection{数据结构} + +\paragraph{TerminatorBase} 语法分析模块指定的语法由终结符和非终结符类型组成,其中终结符和词法分析中的一个词法记号直接对应。为了方便的使用代码描述和操作语法,使用继承对终结符和非终结符进行抽象。终结符类和非终结符类都是继承自同一个基类,并对应的实现中确保类的对象是只读的,这样就可以在后续的算法实现中方便的将它们放在各种哈希表和哈希集合中。 + +\begin{lstlisting}[ + style=csharp +] +public abstract class TerminatorBase +{ + public abstract bool IsTerminated { get; } + + /// + /// 生成能产生该符号的C#代码 + /// 用于预生成分析表 + /// + /// 产生该符号的C#代码 + public abstract string GenerateCode(); +} +\end{lstlisting} + +\paragraph{PascalGrammar类} 此类用于存储和管理Pascal语法的所有产生式。使用上述设计实现的非终结符类和终结符类,可以方便使用哈希表和列表将整个Pascal-S语法用C\#可理解的方式存储在\texttt{PascalGrammar}类中。 + +\paragraph{Expression类} 这个类是项目集的核心,表示带有向前看符号的产生式,用于构建LR分析表和管理解析过程中的状态转移。同样的,这个类也是只读的,方便后续进行各种判等和哈希操作。 + +\begin{lstlisting}[style=csharp] +/// +/// LR语法中的一个表达式,例如 'program_struct -> ~program_head ; program_body' +/// 其中'~'标识当前移进到达的位置 +/// +public class Expression : IEquatable +{ + /// + /// 表达式的左部 + /// + public required NonTerminator Left { get; init; } + + /// + /// 表达式的向前看字符串 + /// + public required Terminator LookAhead { get; init; } + + /// + /// 表达式的右部 + /// + public required List Right { get; init; } + + /// + /// 当前移进的位置 + /// + public required int Pos { get; init; } + + /* 剩下的逻辑略去 */ +} +\end{lstlisting} + +\paragraph{LrState类} 此类用于构建和管理项目集规范族,实现了状态的添加和迁移规则的管理,也是DFA中不同状态的代码抽象。 + +\begin{lstlisting}[style=csharp] +/// +/// LR语法中的一个项目集规范族 +/// 也就是自动机中的一个状态 +/// +public class LrState : IEquatable +{ + /// + /// 项目集规范族 + /// + public required HashSet Expressions { get; init; } + + /// + /// 自动机的迁移规则 + /// + public Dictionary Transformer { get; } = []; +} +\end{lstlisting} + +\subsubsection{核心功能实现} + +\paragraph{从Pascal文法构建语法自动机} 语法分析中的第一个核心功能是从给定的LR(1)文法构建对应的LR(1)自动机,\texttt{GrammarBuilder}类提供了对应的功能。 + +\begin{lstlisting}[style=csharp] +public class GrammarBuilder +{ + /// + /// 指定文法的生成式 + /// + public Dictionary>> Generators { get; init; } = []; + + /// + /// 文法的起始符 + /// + public required NonTerminator Begin { get; init; } + + public Grammar Build() +} +\end{lstlisting} + +在该类中提供了所有在构建自动机过程中需要用到的功能: +\begin{itemize} + \item \texttt{BuildFirstSet} 构建文法中所有非终结符的First集合 + \item \texttt{CalculateFirstSetOfExpression} 计算指定语句的First集合 + \item \texttt{CalculateClosure} 计算指定表达式的项目集规范族闭包 +\end{itemize} + +\paragraph{从LR(1)自动机构建LR(1)分析表} 在构建好LR(1)自动机之后,语法分析模块提供的第二个功能便是从自动机得到对应的分析表。其中\texttt{Grammar}提供了得到内存中分析表的相关功能,\texttt{GeneratedGrammarParser}提供了得到源代码形式分析表的相关功能。对于构建在内存中分析表的\texttt{Grammar}类,其还提供了识别语法中的归约-归约冲突和移进-归约冲突的功能,并且对于语法中存在的ElsePart相关的移进-归约冲突进行了特殊处理。上述的两个类都实现了\texttt{IGrammarParser},这两张分析表都具有了从输入的词法分析流构建自动机的功能。 + +\subsubsection{语法分析相关算法} +\paragraph{求非终结符的First集合} +\begin{figure}[h] +\centering +\includegraphics[width=0.8\linewidth]{assets/FIRST流程图.png} +\caption{求FIRST集合流程图} +\label{fig:求FIRST集合流程图} +\end{figure} + +求FIRST集合的方法如图所示,在编译原理课程中已经详细学习过了,故在此不解释基本原理。代码中BuildFirstSet()即为求FIRST集合的方法,使用了一个变量changed来表示当前一次求闭包的过程有无增加新的元素,以此确定FIRST集合闭包是否已经生成成功。 + +\paragraph{求项目集的产生式闭包} +\begin{figure}[h] +\centering +\includegraphics[width=0.5\linewidth]{assets/项目集闭包流程图.png} +\caption{求项目集产生式闭包流程图} +\label{fig:求项目集产生式闭包流程图} +\end{figure} + +求项目集产生式闭包的方法如图所示。代码中CalculateClosure()即为对应方法。使用哈希值代表一个Expression,可以有效进行表达式的比较。 + +\paragraph{求DFA状态闭包} + +\begin{figure}[htbp] +\centering +\includegraphics[width=0.5\linewidth]{assets/DFA状态闭包流程图.png} +\caption{求DFA状态集闭包流程图} +\label{fig:求DFA状态集闭包流程图} +\end{figure} + +求DFA状态集闭包的方法如图所示。代码中Build()即为对应方法。 + +\paragraph{生成状态转移表} + +状态转移表从DFA生成,可供记号流进行移进-归约而完成语法分析。 +生成状态转移表的流程如下: +\begin{itemize} + \item 构造文法G'的LR(1)项目集规范族C=\{$I_0$,$I_1$,...,$I_n$\},对于状态i(代表项目集$I_i$): + \item 若[A→α·aβ,b]$\in$I, 且go(I,a)=$I_j$, 则置 action[i,a]=$S_j$ + \item 若[A→α·,a]$\in$I,且A不等于S',则置 action[i,a]=$R_j$ + \item 若[S'→S·,\$]$\in$I,则置 action[i,\$]=ACC + \item 若对非终结符号A,有go($I_i$,A)=I,则置goto[i,A]=j + \item 凡是不能用上述规则填入信息的空白表项,均抛出语法分析错误 +\end{itemize} + + +\paragraph{生成语法树} + +生成语法树实际上是利用DFA对输入记号流进行移进-归约的过程实现的。当从栈顶弹出符号时,就是向语法树子节点列表添加子节点的时候;当向栈压入符号时,就是整合子节点列表创建一个语法树父节点的时候;由于使用的是LR(1)自底向上的分析方法,因此最后生成的父节点就是分析器返回给编译程序的语法树根节点,由这个根节点便可以访问整棵语法树。 + +为了加快运行速度,减少生成语法树时重复建立转移表的时间,定义GeneratedTransformer类将转移表转换为可以外部保存的文件形式。编译器可通过读取文件直接载入转移表生成语法树,有效提高了运行效率。 + +\subsection{语义分析} + +\subsubsection{核心数据结构} + +\paragraph{Pascal类型系统} + +为了在语法树上表示变量节点的类型,便于类型传递与检查,需要提供一个Pascal的类型系统。 + +\subparagraph{Pascal类型系统构成} +\begin{figure}[htbp] +\centering +\includegraphics[width=0.5\linewidth]{assets/类型系统.png} +\caption{Pascal类型系统} +\label{fig:类型系统} +\end{figure} + +\begin{itemize} + \item \textbf{PascalType}类 +作为类型系统基类,提供最基本的判等、转换类型、进行数学运算等功能。 + +\begin{lstlisting}[style=csharp] + /// 类型的名称 + public abstract string TypeName { get; } + + /// 将当前类型转换为引用类型 + /// 原有类型变量保持不变 + /// 返回原有Pascal类型的引用类型 + public abstract PascalType ToReferenceType(); + + /// 是否为引用类型 + public bool IsReference { get; init; } +\end{lstlisting} + +\item \textbf{PascalBasicType}类 +继承基类的Pascal基础类型,提供integer,boolean,char,real,void五种基础类型的单例对象。 + +\begin{lstlisting}[style=csharp] + /// 整数类型的单例对象 + public static PascalType Integer => new PascalBasicType(BasicType.Integer); + + /// 布尔类型的单例对象 + public static PascalType Boolean => new PascalBasicType(BasicType.Boolean); + + /// 字符类型的单例对象 + public static PascalType Character => new PascalBasicType(BasicType.Character); + + /// 浮点数类型的单例对象 + public static PascalType Real => new PascalBasicType(BasicType.Real); + + /// 空类型的单例对象 + public static PascalType Void => new PascalBasicType(BasicType.Void); +\end{lstlisting} + +\item \textbf{PascalArrayType}类 +继承基类的Pascal数组类型,表示一个Pascal中的数组类型。 + +\begin{lstlisting}[style=csharp] + // 数组元素的类型 + public PascalType ElementType { get; } = elementType; + + // 数组的起始范围 + public int Begin { get; } = begin; + + // 数组的结束范围 + public int End { get; } = end; + + // 数组的类型名 + public override string TypeName => $"{ElementType.TypeName}_{Begin}_{End}"; +\end{lstlisting} + +\item \textbf{PascalParameterType}类 +继承基类的Pascal参数列表类型,用于过程等需要参数列表的场景。 + +\begin{lstlisting}[style=csharp] + // 参数列表元素类型 + public PascalType ParameterType { get; } = parameterType; + + // 参数列表名称 + public string ParameterName { get; } = parameterName; + + // 参数列表的类型名 + public override string TypeName => $"{ParameterType}_{ParameterName}"; +\end{lstlisting} +\end{itemize} + + +\paragraph{符号表类(SymbolTable)} 符号表类是编译器中用于存储变量与函数声明信息的数据结构,支持编译过程中的多种活动,如变量查找、类型检查和作用域控制,也用于代码生成时获取参数类型和值。 + +\subparagraph{符号表数据结构} +每个符号表类项包括: +\begin{itemize} + + \item 符号表 + + \begin{lstlisting}[style = csharp] + Dictionary _symbols + \end{lstlisting} + +采用字典类型存储符号表内容。键为符号的名字,值为符号类型Symbol。 + + \item 父符号表 + + \begin{lstlisting}[style = csharp] + private readonly SymbolTable? _parent; + \end{lstlisting} + +需要记录父符号表以便于重定位。 + + \item 获得当前符号表的所有祖先符号表 + + \begin{lstlisting}[style = csharp] + public IEnumerable ParentTables => GetParents(); + \end{lstlisting} + +用于在内部作用域查询外部变量信息。 + +\end{itemize} + +\subparagraph{符号类(Symbol)} 符号类是一个Pascal符号在符号表中的存储对象,由类型检查在遍历语法树的过程中创建并存在在符号表中。 + +符号类的数据结构如下所示: +\begin{itemize} + \item 符号名字 + + \begin{lstlisting}[style = csharp] + public required string SymbolName + \end{lstlisting} + + \item 符号类型 + + \begin{lstlisting}[style = csharp] + public required PascalType SymbolType + \end{lstlisting} + + \item 是否为常量 + + \begin{lstlisting}[style = csharp] + public bool Const + \end{lstlisting} + +\end{itemize} + +\subparagraph{符号表的组织} +使用\texttt{栈式哈希符号表}管理不同作用域下的符号信息,通过链表实现了栈式结构的存储。使得每进入一个新的作用域就压入一个新的符号表,每退出一个作用域就弹出当前符号表,并通过parent属性回到父符号表中。 + +\subparagraph{操作定义} +\begin{itemize} + \item \textbf{检索操作}:查找当前及外围作用域中的符号。 + + \begin{lstlisting}[style = csharp] + public bool TryGetSymbol(string name, [NotNullWhen(true)] out Symbol? symbol) + { + if (_symbols.TryGetValue(name, out symbol)) + { + return true; + } + + foreach (SymbolTable table in ParentTables) + { + if (table._symbols.TryGetValue(name, out symbol)) + { + return true; + } + } + + symbol = null; + return false; + } + \end{lstlisting} + + \item \textbf{插入操作}:在符号表中加入新的符号。 + + \begin{lstlisting}[style = csharp] + public bool TryAddSymbol(Symbol symbol) => _symbols.TryAdd(symbol.SymbolName, symbol); + \end{lstlisting} + + \item \textbf{定位与重定位操作}:用于处理作用域的变化,如函数或块的进入和退出。 + + 以过程的定义节点Subprogram为例,进入Subprogram时: + \begin{lstlisting}[style = csharp] + public override void PreVisit(Subprogram subprogram) + { + base.PreVisit(subprogram); + + SymbolTable = SymbolTable.CreateChildTable(); // 创建子符号表并定位 + } + \end{lstlisting} + + 退出Subprogram时: + \begin{lstlisting}[style = csharp] + public override void PostVisit(Subprogram subprogram) + { + base.PostVisit(subprogram); + + if (!SymbolTable.TryGetParent(out SymbolTable? parent)) + { + return; + } + + SymbolTable = parent; // 重定位为父符号表 + } + \end{lstlisting} + +\end{itemize} + +\subsubsection{核心功能说明} + +语义分析模块的核心功能主要分为两个部分:类型检查和代码生成。在编译器的设计中,对于表达式进行类型检查是确保类型安全的重要部分,通过分析表达式中的操作符和操作数,编译器可以确定表达式是否符合语言的类型规则。代码生成则是编译环节的最后步骤,通过结合语法分析和类型检查中获得信息,结合目标语言的各种特征将源语言翻译对指针的目标语言。同时,在代码生成的环节中还需要考虑如何提供源语言中的核心库,例如Pascal中的各种输入和输出函数。 + +为了确保上述功能能够方便的实现,在该模块中实现上述功能的类都继承了语法树上的访问者\texttt{SyntaxNodeVisitor},该类提供了对于语法树进行访问的功能,避免在上述两个类重复的编写访问语法树的逻辑。 + +\subsubsection{类型检查的算法} +在编译器设计中,对表达式进行类型检查是确保类型安全和正确性的关键步骤。通过分析表达式中的操作符和操作数,编译器可以确定表达式是否符合语言的类型规则。 + +为每个非终结符号设定若干综合属性,这些属性由表达式的子部分递归地确定。即语法树的前后序访问操作,会将子树的相关属性向上传播,在上层节点的返回方法中进行检查。这种方法允许编译器在解析表达式时即时地检测和报告类型错误,同时尽可能地更早发现错误。 + +\texttt{ConstDeclaration}:const参数定义赋值。 + +\begin{lstlisting}[style=csharp] +ConstDeclaration -> id = ConstValue | ConstDeclaration ; id = ConstValue +\end{lstlisting} + +ConstDeclaration是const参数定义赋值的非终结符记号,需要使用“向前看”的方法,提前向下获得ConstValue右式的实际值的类型并赋给ConstValue的类型属性。同时,尝试向符号表加入该常量符号。 + +\begin{figure}[h] +\centering +\includegraphics[width=0.4\linewidth]{assets/类型检查/constDeclaration.png} +\caption{ConstDeclaration的属性传递图} +\label{fig:ConstDeclaration} +\end{figure} + +\texttt{Factor}:产生终结符或语句。 + +\begin{lstlisting}[style=grammar] +Factor -> num | Variable + | ( Expression ) + | id () + | id (ExpressionList) + | not Factor + | - Factor + | + Factor + | true + | false +\end{lstlisting} + +Factor作为产生终结符和语句的上层,需要综合下层记号的类型,并向上传递。 + +\begin{figure}[h] +\centering +\includegraphics[width=0.2\linewidth ]{assets/类型检查/Factor.png} +\caption{Factor的属性传递图} +\label{fig:Factor} +\end{figure} + +\texttt{Term}:产生含有Factor的表达式。 + +\begin{lstlisting}[style=grammar] +Term -> Factor | Term MultiplyOperator Factor +\end{lstlisting} + +Term能够产生含有Factor的表达式,因此需要继续承接Factor表达式的类型并向上传递。同时,需要判断MultiplyOperator两侧的记号类型能否参与到运算中。 +特别需要注意的是,不一定要求两侧的记号类型一致。如real和integer,会将计算结果赋予real类型。 + +\begin{figure}[h] +\centering +\includegraphics[width=0.5\linewidth ]{assets/类型检查/Term.png} +\caption{Term的属性传递图} +\label{fig:Term} +\end{figure} + +\texttt{SimpleExpression}:产生含有Term的表达式。 + +\begin{lstlisting}[style=grammar] +SimpleExpression -> Term | SimpleExpression AddOperator Term +\end{lstlisting} + +SimpleExpression能够产生含有Term的表达式,因此需要继续承接Term表达式的类型并向上传递。同时,需要判断AddOperator两侧的记号类型能否参与到运算中。 + +\textit{类型运算之后的结果由Pascal类型系统决定。} + +\begin{figure}[h] +\centering +\includegraphics[width=0.5\linewidth ]{assets/类型检查/SimpleExpression.png} +\caption{SimpleExpression的属性传递图} +\label{fig:SimpleExpression} +\end{figure} + +\texttt{Expression}:产生含有SimpleExpression的表达式。 + +\begin{lstlisting}[style=grammar] +Expression -> SimpleExpression | SimpleExpression RelationOperator SimpleExpression +\end{lstlisting} + +Expression能够产生含有SimpleExpression的表达式,因此需要继续承接SimpleExpression表达式的类型并向上传递。对第二条产生式而言,由于Expression只会产生boolean RelationOperator boolean类型的表达式,故只需要返回boolean类型即可。 + +\begin{figure}[h] +\centering +\includegraphics[width=0.5\linewidth ]{assets/类型检查/Expression.png} +\caption{Expression的属性传递图} +\label{fig:Expression} +\end{figure} + +\texttt{TypeSyntaxNode}:指示记号类型的节点。 + +TypeSyntaxNode指示一个终结符类型。对于普通变量,将其类型直接赋予该节点类型即可。 + +对于数组需要特殊操作。of关键字后的类型将作为数组类型,但对于数组内部的各个维度的范围大小,需要逐层进行检查,以确保表示范围的两个参数均为integer。 + +\begin{figure}[h] +\centering +\includegraphics[width=0.8\linewidth ]{assets/类型检查/TypeSyntaxNode.png} +\caption{TypeSyntaxNode的属性传递图} +\label{fig:TypeSyntaxNode} +\end{figure} + +\texttt{IdentifierList}:参数列表记号节点。 + +\begin{lstlisting}[style=grammar] +IdList -> , id IdList | : Type +\end{lstlisting} + +IdentifierList指示一个产生如函数参数列表的列表记号。 + +这个参数列表的语法树构成如图\ref{fig:IdentifierList},为了区分引用类型和传值类型,需要在访问语法树进入该节点时将SubprogramHead中的IsReference和IsProcedure两个属性继承下来,前者表明这个列表是否为引用类型,通过有无关键字var确定;后者表明这个列表是否用于一个procedure的定义。 + +接下来,在返回过程中,要将从下层节点传上来的节点的类型与当前节点的这两个属性相匹配,生成新的符号放入符号表中。 + +\begin{figure}[htp] +\centering +\includegraphics[width=0.8\linewidth ]{assets/类型检查/IdentifierList.png} +\caption{IdentifierList的属性传递图} +\label{fig:IdentifierList} +\end{figure} + +\texttt{VarDeclaration}:变量列表记号节点。 + +\begin{lstlisting}[style=grammar] +IdList -> , id IdList | : Type +\end{lstlisting} + +VarDeclaration指示一个产生如函数变量列表的列表记号。 + +在返回过程中,要将从下层节点传上来的节点的类型与当前节点的名字这两个属性相匹配,生成新的符号并作为作为一个列表记号放入符号表中。 + +\texttt{Subprogram}:过程节点。 + +\begin{lstlisting}[style=grammar] +Subprogram -> SubprogramHead ; SubprogramBody +\end{lstlisting} + +Subprogram负责作为过程定义的最上层节点。 + +在访问语法树进入该节点时,应当创建子符号表,以隔离内部变量。 +在返回该节点时,应当修改当前符号表为父符号表,以还原符号表层级。 + +\begin{figure}[h] +\centering +\includegraphics[width=0.8\linewidth ]{assets/类型检查/Subprogram.png} +\caption{Subprogram} +\label{fig:Subprogram} +\end{figure} + +\texttt{SubprogramHead}:过程头节点。 + +\begin{lstlisting}[style=grammar] +SubprogramHead -> procedure id FormalParameter + | function id FormalParameter : BasicType +\end{lstlisting} + +SubprogramHead负责作为过程定义的头节点。 + +在访问语法树进入该节点时,应当重置参数表。 + +在返回该节点时,应当向父符号表加入该过程id和其所需参数。如果存在返回值,应当向符号表加入同过程名的返回值类型的变量,表示过程运算结果。 + +观察语法树(图\ref{fig:IdentifierList}),可以注意到参数列表中每个参数的位置实际上与真实位置恰好相反,这主要是因为语法树中靠前的参数在上层,在返回节点收集参数时靠后收集,故需要通过一定的操作将参数列表反转。又因为参数列表由多个不同变量类型的变量列表构成,因此定义一个二维数组,每一行表示一种类型的变量列表,只需要在参数列表中反转这个二维数组即可。 + +\begin{figure}[h] +\centering +\includegraphics[width=0.8\linewidth ]{assets/类型检查/SubprogramHead.png} +\caption{SubprogramHead} +\label{fig:SubprogramHead} +\end{figure} + +\texttt{VarParameter}:引用变量列表。 + +\begin{lstlisting}[style=grammar] +VarParameter -> var ValueParameter +\end{lstlisting} + +VarParameter负责定义引用变量列表。 + +在访问语法树进入该节点时,应当创建一个引用变量列表。 +由于引用变量列表实际上就是通过包装一个变量列表,将其IsReference设为true来实现,故这个节点只实现包装功能。 + +\texttt{ValueParameter}:变量列表。 + +\begin{lstlisting}[style=grammar] +ValueParameter -> id IdList +\end{lstlisting} + +ValueParameter负责定义变量列表。 + +在访问语法树进入该节点时,应当创建一个变量列表。 + +在返回过程中退出该节点时,应当向符号表内加入参数符号,同时向参数列表加入该符号,供上层的VarParameter节点使用。 + +\texttt{Statement}:句子节点,能够产生不同类型的句子,代码检查阶段需要检查这些句子。 + +\begin{itemize} + \item 对于空产生式无需检查 + + \begin{lstlisting}[style=grammar] + Statement -> $\epsilon$ + \end{lstlisting} + + \item 通过在符号表中搜索符号,检查是否定义变量。通过检查符号是否为常量,检查变量能否被再赋值。通过判等变量与表达式的类型,检查变量能否被右侧表达式结果赋值。 + + \begin{lstlisting}[style=grammar] + Statement -> Variable AssignOp Expression + \end{lstlisting} + + \item 对于这个产生式无需检查,相关的类型检查由函数调用的节点负责进行。 + + \begin{lstlisting}[style=grammar] + Statement -> ProcedureCall + \end{lstlisting} + + \item 对于这个产生式无需检查。 + + \begin{lstlisting}[style=grammar] + Statement -> CompoundStatement + \end{lstlisting} + + \item 检查If语句的条件语句Expression是否为boolean。 + + \begin{lstlisting}[style=grammar] + Statement -> Statement -> if Expression then Statement ElsePart + \end{lstlisting} + + \item 通过在符号表中搜索符号,检查是否定义循环变量,通过检查起始表达式类型,判断循环起始是否为整数,通过检查终结表达式类型,判断循环终结是否为整数。 + + \begin{lstlisting}[style=grammar] + Statement -> for id AssignOp Expression to Expression do Statement + \end{lstlisting} + + \item 检查While循环语句的条件变量Expression是否为boolean。 + + \begin{lstlisting}[style=grammar] + Statement -> while Expression do Statement + \end{lstlisting} +\end{itemize} + +\texttt{ProcedureCall}:调用过程 + +编译器将Procedure和Function均视为Procedure,区别在于Procedure不需要参数,而Function需要参数。为此,需要在调用过程区分是否需要参数。 + +在访问语法树退出该节点时,需要进行类型检查。 + +\begin{itemize} + \item 通过在符号表中搜索符号,检查该过程是否定义 + \item 递归判断该符号类型,检查该符号是否能被调用 + \item 通过判断符号内的参数个数属性,检查调用过程时填入的参数个数是否与定义时的参数个数相同 + \item 通过判断符号内的参数类型属性,检查调用过程时填入的参数个数是否与定义时的参数类型相同 +\end{itemize} + +同时,需要将过程的返回值类型赋给过程类型自身,便于上层节点获取。 + +\texttt{Variable}:变量产生节点。 + +\begin{lstlisting}[style=grammar] +Variable -> id IdVarPart +\end{lstlisting} + +Variable负责定义产生一个变量。 + +在返回过程中退出该节点时,应当在符号表中查找这个符号,以确定这个符号已经声明。同时,应获取这个符号的类型并赋给自身的类型属性,便于向上传递变量引用时的类型。 + +若变量为数组类型,则需要利用IdVarPart的综合属性IndexCount,以检查实际赋值时变量维数与定义时维数是否一致。 + +\texttt{IdVarPart}:数组变量下标节点。 + +\begin{lstlisting}[style=grammar] +IdVarPart -> $\epsilon$ | [ ExpressionList ] +\end{lstlisting} + +IdVarPart表示了数组变量的一个元素,如arr[1][1][4]。 + +在语法树访问退出该节点时,需要检查元素下标的每一维是否为integer类型。同时,需要将实际赋值时这个变量的维数作为综合属性IndexCount向上传递,在Varibale中完成进一步的类型检查。 + +\begin{figure}[htbp] +\centering +\includegraphics[width=0.8\linewidth ]{assets/类型检查/IdVarPart.png} +\caption{Variable与IdVarPart} +\label{fig:Variable与IdVarPart} +\end{figure} + +\subsubsection{代码生成的算法} +代码生成是本次编译程序的最终阶段,利用语法树、类型检查阶段得到的信息和符号表,实现pascal代码到C代码的翻译。 + +\paragraph{设计思路} +为非终结符号和终结符号设定若干综合属性和继承属性。在前序遍历到某个语法树节点时,子节点继承属性完成初始化;在后序遍历回到这个语法树节点时,通过子节点的属性计算出该节点的综合属性。在遍历语法树的过程中,利用这些属性信息生成对应的C代码。 + +\paragraph{映射关系} + +\begin{itemize} + \item \textbf{头文件} + + Pascal-S用于输入的read、readln和用于输出的write、writeln分别对应于C程序的scanf和printf,因此需要加入stdio.h头文件。 + + Pascal-S中的基本类型boolean,对应于C语言的bool,但bool不是C语言的基本类型,因此需要加入stdbool.h头文件。 + + \item \textbf{常量和变量} + + Pascal-S中的常量和变量定义对应于C程序的全局常量和全局变量定义。 + + \item \textbf{主程序定义} + + Pascal-S中的主程序可以为任意合法命名,而C语言的主程序名只能为main,因此将Pascal-S主程序名直接翻译为main即可。 + + 此外,Pascal的主程序头还可以包含一个参数列表,这个标识符列表对应于C语言中main函数的参数列表,但是由于Pascal-S缺少响应的库程序支持,因此主程序参数没有任何用途,所以在代码生成阶段,将忽略主程序的参数列表。 + + \item \textbf{子程序声明} + + Pascal-S有procedure和function两种子程序,其中function直接对应于C语言的函数,procedure无返回值,对应于C程序中返回值为void类型的函数。 + + \item \textbf{引用传参} + + Pascal-S中参数列表里带有var声明的为引用传参,对应于C语言中的指针。 + + \item \textbf{函数/过程调用} + + Pascal-S中,不带参数的函数/过程可以不带括号调用,比如func()和func这两种调用方式均可,但C语言中,即使函数/过程调用不含参数,也必须使用一对空括号。 + + 此外,在引用传参时,还要在参数前添加取地址符'\&'。 + + \item \textbf{返回语句} + + Pascal-S的返回语句是通过给函数名赋值来实现的,对应于C语言中需要用到return。 + + \item \textbf{类型关键字} + \begin{table}[h] + \centering + \begin{tabular}{|c|c|} + \hline + \textbf{Pascal-S关键字} & \textbf{C语言关键字}\\ + \hline + integer & int\\ + \hline + real & double\\ + \hline + char & char\\ + \hline + boolean & bool\\ + \hline + \end{tabular} + \end{table} + + \item \textbf{运算符} + + \begin{longtable}{|c|c|c|} + \hline + \textbf{运算符} & \textbf{Pascal-S} & \textbf{C语言}\\ + \hline + \endhead + \hline + \multicolumn{3}{r@{}}{接下一页} + \endfoot + \hline + \endlastfoot + 加 & + & + \\ + \hline + 减 & - & - \\ + \hline + 乘 & * & * \\ + \hline + 除 & / & / \\ + \hline + 整除 & div & / \\ + \hline + 取余 & mod & \% \\ + \hline + 与 & and & \&\& \\ + \hline + 或 & or & || \\ + \hline + 非 & not & \~{} \\ + \hline + 大于 & > & > \\ + \hline + 小于 & < & < \\ + \hline + 大于等于 & >= & >= \\ + \hline + 小于等于 & <= & <= \\ + \hline + 等于 & = & == \\ + \hline + 不等于 & <> & != \\ + \hline + 赋值 & := & = \\ + \hline + \end{longtable} + + \item \textbf{数组} + + \textbf{数组下标 } + Pascal-S中数组上下标均可自定义,数组左边界不必从0开始;但是在C语言中,只能指定数组的上界,下界固定为0。因此我们用Pascal数组的每一维减去当前维度的左边界,将其映射为C语言的数组下标形式。 + + \textbf{数组引用方式 } + 在Pascal-S中,数组的下标引用用一对中括号包裹,每一位维下标之间用逗号分隔。C语言中数组每一维下标都要用中括号包裹。 + 此外,在引用数组元素时,每一个下标都要减去当前维度的左边界 + + \item \textbf{复合语句} + + Pascal-S中符合语句是以begin关键字开始,end关键字结束,对应C语言中一对大括号包括。 + 在Pascal-S中,复合语句的最后一条语句是没有分号的,而C语言中每条语句必须以分号结束。 + + \item \textbf{赋值语句} + + Pascal-S中常量初始化的赋值语句和C语言一样,都是使用'='符号;而变量赋值时,Pascal-S采用':=',C语言采用'='。 + + \item \textbf{表达式} + + 将表达式翻译为三地址代码的形式。 + + \item \textbf{分支语句和循环语句} + + 统一翻译为C语言的goto语句。 + + \item \textbf{输入输出语句} + + Pascal-S在使用read/readln和write/writeln进行输入输出时,将变量作为参数传递即可。而C语言中的scanf和printf函数不仅要传入变量,还需要声明格式化字符串。 + +\end{itemize} + +\paragraph{具体实现} +\begin{itemize} + \item \textbf{头文件的输出} + + 在前序遍历到programHead时就将stdio.h和stdbool.h两个头文件定义输出。 + 输出格式: + \begin{lstlisting}[style = c] + #include + #include + \end{lstlisting} + + \item \textbf{全局常量的输出} + + 在后序遍历到constDeclaration时,从节点中获取常量名和常量值,从符号表里获取常量的类型。输出格式: + \begin{lstlisting}[style = c] + const 常量类型 常量名 = 常量值; + \end{lstlisting} + + \item \textbf{全局变量的输出} + + 变量定义的相关信息存储在了varDeclaration节点的IdList里,于是可以在后序遍历回到IdList时直接从节点里取出类型,标识符名;将类型解析成C语言形式的类型之后,按照如下格式输出: + \begin{lstlisting}[style = c] + 变量类型 变量名; + \end{lstlisting} + + 由于变量定义用到的IdList -> id,IdList 是一个右递归,而每次是后序遍历到varDeclaration时才生成一个变量定义。因此,得到的C代码的变量列表实际上与Pascal的变量列表顺序是相反的,但这并不影响程序的逻辑。 + + 对于数组类型的变量定义输出格式为: + \begin{lstlisting}[style = c] + 数组元素类型 数组名 [维度1][维度2][维度3]...... ; + \end{lstlisting} + + \item \textbf{子函数头的输出} + + 在后序遍历到subprogramHead时,符号表里已经插入了子函数头的相关定义,于是可以直接从符号表里获取函数的返回类型(如果是procedure,返回类型为void)。 + + 接下来是生成参数列表。虽然参数列表的定义里用到了Idlist,但是由于IdList的后序遍历输出的变量列表与定义时的顺序相反,因此不能直接在IdList的后序遍历方法中输出参数列表,函数的参数列表定义必须严格保持其原先的顺序。此时可以直接从函数的类型里将参数列表取出,然后按照[类型] [参数名] 的格式依次输出每一个参数定义即可,参数之间用逗号分隔。(传参是否为引用的判断已经封装在了类型解析里) + + 子函数头的输出格式: + \begin{lstlisting}[style = c] + 返回值类型 函数名 ( 参数列表 ) + \end{lstlisting} + + \item \textbf{函数体的输出} + + Pascal-S中函数体用一个compoundStatement表示,于是在前序遍历到compoundStatement的时候开启一段代码块,输出左大括号;在后序遍历到compoundStatement的时候结束一段代码块,输出右大括号。这一对大括号构成C语言函数体最外层的大括号。 + + \item \textbf{表达式的输出} + + 为了方便后续翻译分支语句和循环语句等,涉及到表达式的语句将会以被翻译成三地址代码的形式。 + + \textbf{三地址代码的生成} + + 观察图\ref{fig:赋值语句的翻译},可以清晰地看到三地址代码的产生过程。在变量没有进行运算时,会直接向上层传递;而每当变量进行一次运算,就会产生一个新的临时变量来存储计算结果(此时会输出这个临时变量的赋值表达式),然后将这个临时变量向上层传递。当传递到statement的时候,整个赋值语句翻译完毕。 + + \begin{lstlisting}[style = c] + //翻译前 + a := a + b * c; + + //翻译后 + int __temp_0 = b * c; + int __temp_1 = a + __temp_0; + a = __temp_1; + \end{lstlisting} + + \begin{figure}[htbp] + \centering + \includegraphics{assets/代码生成/赋值语句的翻译.png} + \caption{赋值语句的翻译} + \label{fig:赋值语句的翻译} + \end{figure} + + + \item \textbf{if语句的翻译} + + 对于if语句的代码生成,需要用到如下辅助栈: + \begin{lstlisting}[style = csharp] + /// + /// 存储IF语句中条件变量的名称 + /// + private readonly Stack _ifConditionNames = new(); + + /// + /// IF语句中成功分支的标签 + /// + private readonly Stack _ifTrueLabels = new(); + + /// + /// IF语句中失败分支的标签 + /// + private readonly Stack _ifFalseLabels = new(); + + /// + /// IF语句中结束的标签 + /// + private readonly Stack _ifEndLabels = new(); + \end{lstlisting} + + \textbf{必要信息入栈} + + 在后序遍历到expression时,将expression的变量名压入\_ifConditionNames栈中; + 在遍历到终结符'then'时,产生翻译if语句需要的三个标签,并压入对应标签栈中。 + + \textbf{使用栈中的属性} + + 当遍历到终结符'then'时,取变量栈栈顶的条件变量名,和相应标签栈栈顶的标签名,按照如下格式输出: + \begin{lstlisting}[style = c] + if (条件变量名) + goto 分支成功标签; + else + goto 分支失败标签; + 分支成功标签:; + \end{lstlisting} + + 当前序遍历到elsePart节点时,由于成功分支不再执行else中的代码,于是需要在此处先输出goto语句,使其跳转到if结束标签处。接着,输出分支失败标签,表示接下来是else的代码段。输出格式如下: + \begin{lstlisting}[style = c] + goto 分支结束标签; + 分支失败标签:; + \end{lstlisting} + + 当后序遍历到elsePart节点时,输出分支结束标签,并将涉及到的变量栈和标签栈栈顶弹出,标志分支翻译结束。输出代码格式如下: + \begin{lstlisting}[style = c] + 分支结束标签:; + \end{lstlisting} + + \textbf{使用栈存储的原因 } 解决嵌套结构的if语句。 + + \item \textbf{for循环的翻译} + + 为了简化翻译的逻辑,将for循环翻译为条件分支if语句和goto语句的组合。 + + 在翻译过程需要用到如下辅助栈: + + \begin{lstlisting}[style = csharp] + /// + /// FOR语句中的循环变量名称 + /// + private readonly Stack _forVariables = new(); + + /// + /// FOR语句中的循环变量的初始值 + /// + private readonly Stack _forBeginConditions = new(); + + /// + /// FOR语句中循环变量的判断值 + /// + private readonly Stack _forEndConditions = new(); + + /// + /// FOR语句开始的标签 + /// + private readonly Stack _forLabels = new(); + + /// + /// FOR语句条件判断部分的标签 + /// + private readonly Stack _forConditionLabels = new(); + + /// + /// FOR语句结束的标签 + /// + private readonly Stack _forEndLabels = new(); + \end{lstlisting} + + \textbf{必要信息入栈供翻译使用} + + 在前序遍历到statement时,如果是for循环语句,将循环变量的变量名名压入\_forVariable栈中;在后序遍历到expression时,如果当前表达式是for循环的循环变量赋初值的表达式,则将expression节点中的变量名压入\_forBeginConditions栈中;如果当前表达式是for循环的边界条件,则将expression节点中的变量名压入\_forEndConditions栈中。 + + \textbf{使用栈中的信息 } + + 在后序遍历到终结符节点'to'时,当前for循环的循环变量及其初值变量必然位于对应栈的栈顶,此时输出循环变量赋初值的C代码,格式为 [循环变量名] = [循环变量初值变量]。此时还应产生for循环要用到的三个标签,并压入栈中,分别为:for循环的条件标签,for循环的主体标签,for循环的结束标签。此时输出for循环的条件标签,表示接下来的内容为条件判断部分。输出格式为: + + \begin{lstlisting}[style = c] + 循环变量名 = 初值变量; + for循环条件标签:; + \end{lstlisting} + + 在后序遍历到终结符节点'do'时,当前for循环的边界条件变量位于\_forEndConditions栈的栈顶。此时输出条件判断部分,此外还应输出for循环主体标签,表示接下来的内容为for循环主体。输出格式: + + \begin{lstlisting}[style = c] + if ( 循环变量 <= 边界变量 ) + goto for循环主体标签; + else + goto for循环结束标签 + for循环主体标签:; + \end{lstlisting} + + 在后序遍历回到产生for循环的statement节点时,输出更新循环变量的C语言语句,并用goto语句跳转到条件判断标签处(这一步是实现“循环”的关键)。接着,输出for循环结束标签,将对应变量栈和标签栈的栈顶弹出,标志for循环翻译完毕。 + 输出格式: + \begin{lstlisting}[style = c] + 循环变量 = 循环变量 + 1; + goto for循环条件标签; + for循环结束标签:; + \end{lstlisting} + + \item \textbf{while循环的翻译} + + 与for循环类似,将while语句也翻译为条件分支if语句和goto语句的组合。 + + 此时要用到的辅助栈如下: + \begin{lstlisting}[style = csharp] + /// + /// WHILE语句条件变量的标签 + /// + private readonly Stack _whileConditionNames = new(); + + /// + /// WHILE语句开始的标签 + /// + private readonly Stack _whileBeginLabels = new(); + + /// + /// WHILE语句结束的标签 + /// + private readonly Stack _whileEndLabels = new(); + \end{lstlisting} + + 在先序遍历到expression节点时,产生while循环要用到的三个标签并压入栈中。此时输出while循环条件标签,表示接下来的程序段为while循环条件判断,输出格式如下: + \begin{lstlisting}[style = c] + while循环条件标签:; + \end{lstlisting} + + 在后序遍历到终结符节点'do'时,输出while条件判断内容,格式如下: + \begin{lstlisting}[style = c] + if(while条件变量 == false) + goto while结束标签; + \end{lstlisting} + + 在后序遍历到产生while语句的statement节点时,输出goto语句跳转到while条件判断标签处。接着,输出while循环结束标签,并将对应变量栈和标签栈弹出,while循环翻译完毕。输出格式: + \begin{lstlisting}[style = c] + goto while循环条件标签; + while循环结束标签:; + \end{lstlisting} + + \item \textbf{函数/过程调用的翻译} + + 由于Factor和ProcedureCall都能够产生函数/过程调用语句,因此在后序遍历回到这两个节点时,都要生成函数调用的代码。 + + 对于引用传递的参数,需要在输出的参数前添加取地址符'\&'。此外,还需要特殊处理函数内部的递归。 + + 输出格式: + \begin{lstlisting}[style = c] + 函数名( 参数1, 参数2......) + \end{lstlisting} + + \item \textbf{输入输出语句的翻译} + + 输入输出语句会被当作普通的函数调用语句来处理,但是区别是需要在参数列表之前添加格式化字符串。此外,在scanf的参数列表中,每一个参数前需要添加取地址符'\&'。格式如下: + \begin{lstlisting}[style = c] + //输入 + scanf("%d", &a) + //输出 + printf("%d", a) + \end{lstlisting} + + \item \textbf{main函数的翻译} + + main函数生成的位置其实和Pascal-S主程序体的位置是一样的,但是需要在适当的时机生成main函数头和main函数的返回语句。 + + 由于每一个函数体都对应一个compoundStatement,于是我们在该节点上设置属性IsMain,在先序遍历到programBody时将主程序体对应的compoundStatement的IsMain设置为true。在先序遍历到compoundStatement时,生成main函数头,格式为: + \begin{lstlisting}[style = c] + int main() + \end{lstlisting} + 在后序遍历回到compoundStatement节点时,如果IsMain为true,生成main函数返回语句,输出格式: + \begin{lstlisting}[style = c] + return 0; + \end{lstlisting} + + \item \textbf{处理短路问} + 对于布尔表达式,需要处理短路的问题,主要是针对and和or操作。需要用到的辅助数据结构如下: + \begin{lstlisting}[style = csharp] + + private record CircuitLabel(string Circuit, string End); + + private readonly Stack _andCircuitLabels = []; + + private readonly Stack _orCircuitLabels = []; + \end{lstlisting} + + \textbf{and的短路} + + 在后序遍历到终结符'and'时,取and左侧变量名,取\_andCircuitLabels栈中的and短路标签,输出格式: + \begin{lstlisting}[style = c] + if(!左侧变量名) + goto 短路标签; //如果and左操作数为false,则无需判断右操作数 + \end{lstlisting} + + 在后序遍历到term节点时,输出如下C代码: + \begin{lstlisting}[style = c] + bool temp = 左侧变量 && 右侧变量; //执行与操作 + goto 短路结束标签; //跳过短路代码段 + 短路标签:; + temp = false; //被短路时,直接设置条件为false + 短路结束标签:; + \end{lstlisting} + + \textbf{or的短路} + + 在后序遍历到终结符'or'时,取or左侧变量,取\_orCircuitLabels栈顶的短路标签,按如下格式输出: + \begin{lstlisting}[style = c] + if(左侧变量名) + goto 短路标签; //如果or左操作数为true,则无需判断右操作数 + \end{lstlisting} + + 在后序遍历到simpleExpression节点时,输出如下C代码: + \begin{lstlisting}[style = c] + bool temp = 左侧变量 or 右侧变量; //执行或操作 + goto 短路结束标签; //跳过短路代码段 + 短路标签:; + temp = true; //被短路时,直接设置条件为true + 短路结束标签:; + \end{lstlisting} + +\end{itemize} + +\end{document} \ No newline at end of file diff --git a/docs/contents/general-design.tex b/docs/contents/general-design.tex new file mode 100644 index 0000000..3cf5fdf --- /dev/null +++ b/docs/contents/general-design.tex @@ -0,0 +1,459 @@ +\documentclass[../main.tex]{subfiles} + +\begin{document} +\section{总体设计} + +% 说明,包括: +% 1)数据结构设计 +% 2)总体结构设计:包括 +% 功能模块的划分 +% 模块功能 +% 模块之间的关系 +% 模块之间的接口 +% 3)用户接口设计 + +\subsection{数据流图} + +\begin{figure}[h] + \centering + \includegraphics[width=0.9\linewidth]{assets/数据流图.png} + \caption{数据流图} + \label{fig:data_flow_diagram} +\end{figure} + +\subsection{数据结构设计} + +在整个编译器设计中,数据结构的设计是至关重要的一环。它不仅需要支持编译器的各个阶段,还需要保证数据的正确传递和高效处理。在本节中将对编译器中各个模块之间共有的部分数据结构进行说明。 + +\subsubsection{词法记号} + +\texttt{SemanticToken} 是一个抽象基类,定义了所有词法记号的共有属性和方法。具体类型的词法记号(如关键字、标识符等)都继承自这个类。 + +每个Token至少有四个属性:记号类型 \texttt{SemanticTokenType TokenType},行号 \texttt{uint LinePos},字符位置 \texttt{uint CharacterPos},字面量 \texttt{string LiteralValue}。 + +\begin{lstlisting}[ + style=csharp +] +public abstract class SemanticToken +{ + public abstract SemanticTokenType TokenType { get; } + + /// + /// 记号出现的行号 + /// + public required uint LinePos { get; init; } + + /// + /// 记号出现的列号 + /// + public required uint CharacterPos { get; init; } + + /// + /// 记号的字面值 + /// + public required string LiteralValue { get; init; } +} +\end{lstlisting} + +实际继承词法记号基类的词法记号类有: +\begin{itemize} +\item 字符类型记号 \texttt{CharacterSemanticToken} +\item 字符串类型记号 \texttt{StringSemanticToken} +\item 分隔符类型记号 \texttt{DelimiterSemanticToken} +\item 关键词类型记号 \texttt{KeywordSemanticToken} +\item 操作符类型记号 \texttt{OperatorSemanticToken} +\item 数值类型记号 \texttt{NumberSemanticToken} +\item 标识符类型记号 \texttt{IdentifierSemanticToken} +\end{itemize} + +其中分隔符类型、关键词类型、操作符类型等记号提供一个属性获得该记号代表的分隔符、关键词、操作符,这些可以穷举的类型使用枚举标识,在表\ref{table:operator_and_delimiter}和表\ref{table:keyword_and_operator}中列举了所有的分隔符、关键词和操作符。而对于字符类型记号,字符串类型记号、数组类型记号,在代码中分别提供了将字面值识别为C\#中的字符、字符串和数值等类型的功能,方便在代码中对于这种固定值进行操作。在标识符类型中则是提供了一个返回标识符值的方法,在该方法中会自动将字面值小写,以此来提供Pascal代码中对于大小写不敏感的功能。 + +% \begin{table}[h] +% \centering +% \caption{基本类型和标识符} +% \begin{tabular}{|c|c|c|c|} +% \hline +% \textbf{描述} & \textbf{字面量记录} & \textbf{记号类型} & \textbf{详细类型} \\ +% \hline +% 标识符 & 该标识符本身 & IDENTIFIER & \\ +% 无符号整数 & 该整数本身(字符串表示) & NUMBER & 整数 \\ +% 无符号浮点数 & 该浮点数本身(字符串表示) & & 实数 \\ +% 十六进制数 & 该十六进制数本身(字符串表示) & & 十六进制 \\ +% 字符常量 & 该字符常量本身(不包含两侧的单引号) & CHARACTER & \\ +% \hline +% \end{tabular} +% \end{table} + +\begin{longtable}{|c|c|c|c|} +\caption{运算符和分界符} \label{table:operator_and_delimiter} \\ +\hline +% 跨页表的第一行 +\textbf{描述} & \textbf{字面量记录} & \textbf{记号类型} & \textbf{详细类型} \\ +\hline +\endhead +% 跨页表的最后一行 +\hline +\multicolumn{4}{r@{}}{接下一页} +\endfoot +% 跨页表的最后一页的最后一行 +\hline +\endlastfoot +关系运算符 & $\geq$ & Operator & 大于等于 \\ + & $>$ & & 大于 \\ + & $\leq$ & & 小于等于 \\ + & $\neq$ & & 不等于 \\ + & $<$ & & 小于 \\ +关系运算符:相等 & $=$ & & 等于 \\ +算术运算符:加法 & $+$ & & 加 \\ +算术运算符:减法 & $-$ & & 减 \\ +算术运算符:乘法 & $*$ & & 乘 \\ +算术运算符:除法 & $/$ & & 除 \\ +赋值符号 & $:=$ & & 赋值 \\ +范围连接符 & $..$ & Delimiter & 点点 \\ +界符 & $($ & & 左括号 \\ + & $)$ & & 右括号 \\ + & $[$ & & 左方括号 \\ + & $]$ & & 右方括号 \\ + & $:$ & & 冒号 \\ + & $,$ & & 逗号 \\ + & $;$ & & 分号 \\ + & $.$ & & 句号/点 \\ +\hline +\end{longtable} + +\begin{longtable}{|c|c|c|c|} +\caption{关键字和逻辑运算符} \label{table:keyword_and_operator} \\ +\hline +\textbf{描述} & \textbf{字面量记录} & \textbf{记号类型} & \textbf{详细类型} \\ +\hline +\endhead +% 跨页表的最后一行 +\hline +\multicolumn{4}{r@{}}{接下一页} +\endfoot +% 跨页表的最后一页的最后一行 +\hline +\endlastfoot +逻辑运算符:或 & or & Keyword & 或 \\ +算术运算符:取余 & mod & & 取余 \\ +逻辑运算符:且 & and & & 且 \\ +逻辑运算符:非 & not & & 非 \\ +关键字 & program & & 程序 \\ +关键字 & const & & 常量 \\ +关键字 & var & & 变量 \\ +关键字 & array & & 数组 \\ +关键字 & of & & 属于 \\ +关键字 & procedure & & 过程 \\ +关键字 & function & & 函数 \\ +关键字 & begin & & 开始 \\ +关键字 & end & & 结束 \\ +关键字 & if & & 如果 \\ +关键字 & then & & 那么 \\ +关键字 & for & & 对于 \\ +关键字 & to & & 至 \\ +关键字 & do & & 执行 \\ +关键字 & else & & 否则 \\ +关键字 & repeat & & 重复 \\ +关键字 & until & & 直到 \\ +关键字 & while & & 当 \\ +关键字 & integer & & 整数 \\ +关键字 & real & & 实数 \\ +关键字 & char & & 字符 \\ +关键字 & boolean & & 布尔 \\ +\end{longtable} + +\subsubsection{语法树} + +语法树是编译器中用于表示源代码结构的树状数据结构。在语法分析阶段,编译器将源代码转换为语法树,以便后续阶段可以更高效地进行处理。因此,语法树中每个节点和语法中的每个符号一一对应,其中非终结符即对应书上的父节点,终结符对应了树上的叶子节点。 + +在终结节点上直接封装了访问对应的词法分析令牌的功能。 + +\begin{lstlisting}[style=csharp] +public class TerminatedSyntaxNode : SyntaxNodeBase +{ + public override bool IsTerminated => true; + + public required SemanticToken Token { get; init; } + + // 其他代码有删节 +} +\end{lstlisting} + +在针对不同的非终结节点,首先在其的共同基类\texttt{NonTerminatedSyntaxNode}中封装了访问其子节点的功能,并针对该节点产生式的不同提供了不同的方式模型。 + +针对只有一个产生式的非终结节点,直接在该非终结节点上使用属性的方式将其有意义的子节点暴露出来,例如在\texttt{ProgramStruct}上就直接暴露放访问\texttt{ProgramHead}的属性。 + +\begin{lstlisting}[style=csharp] +public class ProgramStruct : NonTerminatedSyntaxNode +{ + public override NonTerminatorType Type => NonTerminatorType.ProgramStruct; + + /// + /// 程序头 + /// + public ProgramHead Head => Children[0].Convert(); +} +\end{lstlisting} + +针对含有多个产生式的非终结节点,如果是有效的子节点只有一种的,则仍然使用属性的方式进行暴露,例如\texttt{ConstDeclaration},其就暴露了标识符名称和值两个属性。 + +\begin{lstlisting}[style=csharp] +public class ConstDeclaration : NonTerminatedSyntaxNode +{ + public override NonTerminatorType Type => NonTerminatorType.ConstDeclaration; + + /// + /// 是否递归的声明下一个ConstDeclaration + /// + public bool IsRecursive { get; private init; } + + /// + /// 获得声明的常量 + /// + public (IdentifierSemanticToken, ConstValue) ConstValue => GetConstValue(); + + public static ConstDeclaration Create(List children) + { + bool isRecursive; + if (children.Count == 3) + { + isRecursive = false; + } + else if (children.Count == 5) + { + isRecursive = true; + } + else + { + throw new InvalidOperationException(); + } + + return new ConstDeclaration { Children = children, IsRecursive = isRecursive }; + } + + private static IdentifierSemanticToken ConvertToIdentifierSemanticToken(SyntaxNodeBase node) + { + return (IdentifierSemanticToken)node.Convert().Token; + } + + private (IdentifierSemanticToken, ConstValue) GetConstValue() + { + if (IsRecursive) + { + return (ConvertToIdentifierSemanticToken(Children[2]), Children[4].Convert()); + } + else + { + return (ConvertToIdentifierSemanticToken(Children[0]), Children[2].Convert()); + } + } +} +\end{lstlisting} + +而对于使用的多个产生式且无法有效提取信息的非终结节点,则设计使用\textbf{事件}以提供相关信息的功能。访问者可以在需要使用对应产生式的信息时订阅对应的事件,并且语法树的实现保证对应的事件会在第一次访问和第二次访问时按照订阅的顺序进行调用。对应事件的事件参数也可提供产生式相关的信息。 + +\texttt{ConstValue}就是一个不错的例子,其提供了使用数值产生式和字符产生式的两个事件供订阅。 + +\begin{lstlisting}[style=csharp] +/// +/// 使用数值产生式事件的事件参数 +/// +public class NumberConstValueEventArgs : EventArgs +{ + /// + /// 是否含有负号 + /// + public bool IsNegative { get; init; } + + /// + /// 数值记号 + /// + public required NumberSemanticToken Token { get; init; } +} + +/// +/// 使用字符产生式事件的事件参数 +/// +public class CharacterConstValueEventArgs : EventArgs +{ + /// + /// 字符记号 + /// + public required CharacterSemanticToken Token { get; init; } +} + +public class ConstValue : NonTerminatedSyntaxNode +{ + public override NonTerminatorType Type => NonTerminatorType.ConstValue; + + /// + /// 使用数值产生式的事件 + /// + public event EventHandler? OnNumberGenerator; + + /// + /// 使用字符产生式的事件 + /// + public event EventHandler? OnCharacterGenerator; +} +\end{lstlisting} + +\subsubsection{符号表} + +符号表是在语义分析阶段使用的数据结构,用于存储变量、函数和过程的信息。符号表支持查询、插入和作用域管理操作。每个作用域都有自己的符号表,如果当前作用域中没有找到符号,则会递归查询父作用域。 + +符号表的设计如下: + +\begin{itemize} + \item \textbf{符号表项(SymbolTableItem)}:包含类型(MegaType)、名称、是否为变量、是否为函数和参数列表。 + \item \textbf{类型(MegaType)}:包含指针类型和项类型。 +\end{itemize} + +符号表的物理结构采用哈希表实现,以支持高效的查询和插入操作。 + +\subsubsection{语法树上的旅行者} + +在语法分析完成对于语法树的构建之后,我们需要在语法树的各个节点上进行一系列的操作,例如进行符号表的维护、类型检查和代码生成等任务。为了降低程序的复杂度,我们希望在程序中提供一个统一的语法树遍历和访问接口。因此,我们使用访问者设计模式设计了\texttt{SyntaxNodeVisitor}(语法节点访问者)和\texttt{SyntaxTreeTraveller}(语法树旅行者)。同时结合编译原理课程中语义分析和翻译方案相关的知识,我们设计了一种称为\textit{前后序遍历}的语法树访问模型。例如对于图\ref{fig:syntax_tree_example}中的一颗语法树,其的遍历顺序为 + +\begin{align}\notag +& ProgramStruct \to ProgramHead \to program \to program \to main \to main \\ \notag +&\to ProgramHead \to ; \to ; \to ProgramBody \to ConstDeclarations \to \\ \notag +&ConstDelcarations \to VarDeclarations \to VarDeclarations \to \\ \notag +&SubprogramDeclarations \to SubprogramDeclarations \to CompoundStatement \\ \notag +&\to begin \to begin \to StatementList \to Statement \to Statement \to \\ \notag +&StatementList \to end \to end \to CompoundStatement \to ProgramBody \\ \notag +&\to . \to . \to ProgramStruct \notag +\end{align} + +\begin{figure}[t] + \centering + \includegraphics[width=0.9\linewidth]{assets/示例语法树图.png} + \caption{示例的语法树图} + \label{fig:syntax_tree_example} +\end{figure} + +在设计对于语法树的遍历之后,我们在设计了对于语法节点的访问者,访问者针对语法树上的每一个节点都提供了两个访问接口,分别会在第一次遍历到该节点和第二次遍历到该节点时调用,称为\texttt{PreVisit}和\texttt{PostVisit}。按照编译原理课程中的知识来说,\texttt{PreVisit}接口理解为对于该节点的L-属性计算,\texttt{PostVisit}接口理解为对该节点的S-属性计算。 + +为了使得各语义分析的工作可以方便的组合在一起运行,例如类型检查需要在代码检查之前运行,容易想到使用类型继承的方式进行抽象。例如类型检查类直接继承了语法节点访问者抽象基类\texttt{SyntaxNodeVisitor},而代码生成了则直接继承了类型检查类。需要注意的是,在重载访问语法节点的接口函数之间,需要在执行任何操作之前调用基类的对应操作。 + +\begin{lstlisting}[ + style=csharp, + caption={示例的代码生成类代码} +] +public class CodeGeneratorVisitor(ICompilerLogger? logger = null) : TypeCheckVisitor(logger) +{ + public override void PreVisit(ProgramHead programHead) + { + // 调用基类的访问方法 + base.PreVisit(programHead); + + // 实际的代码生成逻辑... + } +} +\end{lstlisting} + +\subsection{总体结构设计} + +\textit{Canon}编译器的核心库按照编译的工作流程和相关工作划分为各个模块: +\begin{itemize} + \item 源代码读取模块 + \item 词法分析模块 + \item 语法分析模块 + \item 语义分析模块 + \item 日志输出模块 +\end{itemize} + +鉴于项目中主要使用依赖注入的设计模块进行开发,因此各个模块都提供了对应接口。下面首先介绍各个模块之前的接口,然后将分模块介绍各个模块的功能。 + +\subsubsection{模块提供的接口} + +\paragraph{ISourceReader} 源代码读取模块提供的接口。该接口在提供文件读取函数的同时,还提供了读取的缓冲区功能,在获得当前读取字符及行号、列号的同时,可以前进读取一个字符,后退一个字符,最后尝试读取下一个字符。 + +\paragraph{ILexer} 词法分析器的接口。该接口提供了从源代码中分析为一个语法分析流的功能。 + +\paragraph{IGrammarParser} 语法分析模块的接口。该接口提供了将一个词法分析流构建为一颗语法树的功能。 + +\paragraph{SyntaxNodeVisitor} 语法树节点访问抽象类。该接口提供了对于语法树上各个节点的访问方法。 + +\paragraph{ICompilerLogger} 编译日志输出接口。该接口提供了输出各个等级信息的能力。 + +\subsubsection{词法分析模块} + +词法分析模块负责读入输入字符串,解析为词法记号流输出。 + +\subsubsection{语法分析模块} + +语法分析模块主要负责从Pascal语法构建LR(1)分析表和对输入的词法记号流进行分析构建语法树的工作。 + +对于语法分析模块而言,LR(1)分析表存在两种表现形式:(1) 内存形式,直接通过Pascal-S语法分析并构建自动机进而得到的分析表;(2)源代码形式,鉴于每次都从Pascal-S语法进行分析并构建自动机消耗的时间和资源非常多,而且语法在大多数时间都是不变的,因此我们实现了将LR(1)分析表生成到C\#源代码形式的功能。 + +因此语法分析模块主要提供三个功能:从语法构建自动机并得到LR(1)分析表;将LR(1)分析表生成为C\#源代码形式;从分析表分析输入的语法分析流并构建语法树。 + +\subsubsection{语义分析模块} + +语义分析模块负责完成类型检查和代码生成两个功能。为了完成上述的工作,在语义分析模块中实现了Pascal-S语言的类型系统和对于语法树的访问和遍历逻辑。 + +\subsection{用户接口设计} + +\subsubsection{命令行版本} + +命令行版本的接口设计旨在为用户提供一个简单、直接的方式来使用编译器。用户可以通过命令行工具 \texttt{Canon Pascal Compiler} 来转换 Pascal 源代码文件到 C 代码文件。 + +使用方法如下: + +\begin{verbatim} +Canon.Console [options] +Options: + -i, --input (REQUIRED) Pascal源代码文件地址 + --version 显示版本信息 + -?, -h, --help 显示帮助信息 +\end{verbatim} + +其中 \texttt{} 是必须提供的 Pascal 源文件路径。命令行版本支持以下特性: + +\begin{itemize} + \item \textbf{参数解析}:通过 \texttt{System.CommandLine} 库解析命令行参数,提供灵活的命令行选项。 + \item \textbf{日志记录}:使用 \texttt{CompilerLogger} 类记录编译过程中的信息,帮助用户了解编译状态。 +\end{itemize} + +\subsubsection{Web在线版本} +\begin{figure}[h] + \centering + \includegraphics[width=0.9\linewidth]{assets/编译器Web在线版本.png} + \caption{编译器Web在线版本} + \label{fig:compiler_web_fig} +\end{figure} + +Web在线版本提供了一个图形化界面,允许用户在网页上直接输入Pascal 源代码,并在线编译和查看生成的 C 代码。这为没有命令行使用经验的用户提供了便利(图\ref{fig:compiler_web_fig})。同时,图形化界面提供了Pascal源代码生成的语法树示意图(图\ref{fig:compiler_web_fig_tree}),可供用户查看并分析语法树结构。 + +\begin{figure}[h] + \centering + \includegraphics[width=0.9\linewidth]{assets/编译器Web在线版本_语法树.png} + \caption{语法树渲染} + \label{fig:compiler_web_fig_tree} +\end{figure} + +Web版本的特点包括: + +\begin{itemize} + \item \textbf{代码编辑器}:集成代码编辑器,支持语法高亮,提供更好的代码编写体验。 + \item \textbf{实时编译}:用户输入代码后,可以实时编译并显示输出结果。 + \item \textbf{错误提示}:编译过程中的错误会在网页上直接显示,方便用户快速定位问题。 + \item \textbf{语法树渲染}:编译过程中,会根据输入的代码,渲染出对应的语法树。语法树上节点对应的记号类型。 + \item \textbf{历史记录}:编译器会保存成功编译的记录,并提供查看历史记录的功能。使用唯一id作为历史记录标识,实现了通过连接分享一个编译记录的功能(图\ref{fig:compiler_web_fig_history})。 +\end{itemize} + +\textit{注: 在实现语法树的可视化过程中,我们参考了论文\cite{goos_improving_2002}以在线性时间复杂度中绘制完整棵树。} + +\begin{figure}[h] + \centering + \includegraphics[width=0.9\linewidth]{assets/编译器Web在线版本_历史记录.png} + \caption{历史记录} + \label{fig:compiler_web_fig_history} +\end{figure} + +Web在线版本的实现依赖于前后端分离的架构,前端使用React框架提供用户交互界面,后端处理编译任务。通过 AJAX 请求与后端通信,实现代码的提交和结果的获取。 + +总体来说,用户接口设计考虑了不同用户群体的使用习惯和需求,提供了灵活、友好的使用方式,使得用户可以更加方便地使用。 + +\end{document} \ No newline at end of file diff --git a/docs/contents/program-test.tex b/docs/contents/program-test.tex new file mode 100644 index 0000000..164d7ed --- /dev/null +++ b/docs/contents/program-test.tex @@ -0,0 +1,1255 @@ +\documentclass[../main.tex]{subfiles} +\begin{document} +\section{程序测试} + +% 给出测试报告,包括: +% 1)测试环境 +% 2)测试的功能 +% 3)针对每个功能的测试情况,包括:测试用例、预期的结果、测试结果及其分析 +% 在设计测试计划时,不但要考虑正确的测试用例,还要考虑含有错误的测试用例 + +\subsection{测试环境} + +本项目的测试分为单元测试、集成测试两个部分。其中,单元测试基于Xunit框架使用代码编写,集成测试则是将Pascal源文件分别使用\texttt{Free Pascal Compiler}编译器和自行实行的编译器之后运行之后对比输出。 + +\paragraph{本地测试环境} +\begin{itemize} + \item \textbf{操作系统}: Arch Linux 6.8.9-zen1-2-zen (64-bit) + \item \textbf{处理器}: 16 × 11th Gen Intel® Core™ i7-11800H @ 2.30GHz + \item \textbf{内存}: 15.4 GiB of RAM + \item \textbf{编译器}: Dotnet 8.0.104 + \item \textbf{开发环境}: JetBrains Rider +\end{itemize} + +\paragraph{集成测试平台} +\begin{itemize} + \item \textbf{操作系统}: Arch Linux 6.8.9-zen1-2-zen (64-bit) + \item \textbf{处理器}: 16 × 11th Gen Intel® Core™ i7-11800H @ 2.30GHz + \item \textbf{内存}: 15.4 GiB of RAM + \item \textbf{C语言编译器}: GCC 14.1.1 + \item \textbf{Free Pascal Compiler编译器}: Free Pascal Compiler version 3.2.2 +\end{itemize} + +\subsection{单元测试} + +单元测试需要对程序的一个或多个部分进行测试,因此不仅对本部分功能进行测试,还对与其他部分的协同工作进行测试。以下先介绍代码中各个目录涉及到的测试功能,再根据逻辑上的编译器划分进行逐部分功能说明。 + +\begin{itemize} + \item \texttt{SemanticTests} 对字符(SemanticToken)的识别功能进行测试。 + \item \texttt{LexicalParserTests} 对词法分析器(Lexer)进行词法单元识别功能测试。 + \item \texttt{GrammarParserTests} 对语法分析器(GrammarParser)进行正确性和稳健性测试。 + \item \texttt{GeneratedParserTests} 对GeneratedGrammarParser能否正确生成语法解析进行测试。 +\end{itemize} + +\subsubsection{词法分析} + +词法分析的功能为:将输入的Pascal程序按照一定规则以记号流的方式输出给后面的分析器,因此此处测试的主要任务应该是检查词法分析器能否正确识别程序中的"词语",并按照记号流的方式输出。因此我们通过对单一类型记号的测试,多种类型记号的混合测试,完整程序的验证测试来完成对词法分析的检查。 + +\begin{table}[H] % 使用 [H] 选项将表格固定在当前位置 +\centering % 让表格内的文字居中 +\caption{ +\label{tab:test}词法分析功能说明} +\begin{tabular}{c|p{10cm}} % 修改第一列为居中显示 +\hline +\textbf{测试文件} & \textbf{功能} \\ +\hline +CharacterTypeTests.cs & 测试对语句能否正确分隔,未定义符号能否识别报错 \\ +DelimiterTests.cs & 对Delimiter分隔符的识别 \\ +ErrorSingleTests.cs & 对字符串中的错误识别,以及非法字符的识别 \\ +IndentifierTypeTests.cs & 标识符,与标识符类似的记号 \\ +KeywordTypeTests.cs & 关键字识别 \\ +LexicalFileTests.cs & 对词法分析器的总体功能测试,手动打表验证对pascal文件读取后的记号流输出。以及能否正确跳过注释 \\ +NumberTests.cs & 对科学计数法、浮点数、十六进制等不同类型数字的识别 \\ +OperatorTypeTests.cs & 对运算符的识别 \\ +\hline +\end{tabular} +\end{table} + +\paragraph{基础测试} +通过简单的输入,如数字序列和简单运算表达式,验证词法分析器是否能正确分辨和生成对应的标记。 + +\begin{itemize} + \item \textbf{输入}: + \begin{verbatim} + 123 + 456 - 789 + \end{verbatim} + \item \textbf{预期输出}: + \begin{itemize} + \item 三个 \texttt{TOK\_INTEGER} 分别对应数值 123, 456, 789 + \item 一个 \texttt{TOK\_PLUS} 和一个 \texttt{TOK\_MINUS} 对应加号和减号 + \end{itemize} +\end{itemize} + +\paragraph{注释} +验证词法分析器是否能正确忽略Pascal代码中的单行和多行注释。 + +\begin{itemize} + \item \textbf{输入}: + \begin{verbatim} + // This is a single line comment + begin end. + \end{verbatim} + \item \textbf{预期输出}: + \begin{itemize} + \item 词法分析器应忽略注释,正确识别 \texttt{begin} 和 \texttt{end.} 标记 + \end{itemize} +\end{itemize} + +\begin{itemize} + \item \textbf{输入}: + \begin{verbatim} + { + This is a + multi-line comment + } + begin end. + \end{verbatim} + \item \textbf{预期输出}: + \begin{itemize} + \item 注释被正确忽略,仅识别 \texttt{begin} 和 \texttt{end.} 标记 + \end{itemize} +\end{itemize} + +\paragraph{标识符和关键字} +验证词法分析器对Pascal关键字和用户定义的标识符的识别能力。 + +\begin{itemize} + \item \textbf{输入}: + \begin{verbatim} + program test; var x: integer; + \end{verbatim} + \item \textbf{预期输出}: + \begin{itemize} + \item 关键字 \texttt{program}, \texttt{var}, \texttt{integer} 以及标识符 \texttt{test}, \texttt{x} 被正确识别 + \end{itemize} +\end{itemize} + +\paragraph{字符串和字符识别} +测试词法分析器对字符串和字符字面量的处理。 + +\begin{itemize} + \item \textbf{输入}: + \begin{verbatim} + 'Hello, World!' "A" + \end{verbatim} + \item \textbf{预期输出}: + \begin{itemize} + \item 一个 \texttt{TOK\_STRING} 为 "Hello, World!" + \item 一个 \texttt{TOK\_CHAR} 为 'A' + \end{itemize} +\end{itemize} + +\paragraph{CharacterTypeTests.cs} +此测试类用于验证字符和字符串类型的正确识别,确保词法分析器可以正确处理单个字符和字符串。 + +\textbf{测试方法:}使用ILexer接口的Tokenize方法处理输入的字符和字符串,然后检查生成的SemanticToken对象是否与预期的类型和值匹配。 + +\textbf{关键代码片段:} +\begin{lstlisting}[style=csharp] +[InlineData("'a'", 'a')] +[InlineData("'+'", '+')] +public void TestCharacterType(string input, char expectedResult) +{ + IEnumerable tokensEnumerable = _lexer.Tokenize(new StringSourceReader(input)); + List tokens = tokensEnumerable.ToList(); + Assert.Equal(SemanticTokenType.Character, tokens[0].TokenType); + Assert.Equal(expectedResult, tokens[0].Convert().ParseAsCharacter()); +} +\end{lstlisting} + +通过上述测试,词法分析器能正确识别并返回字符类型的SemanticToken。 + +\paragraph{DelimiterTests.cs} + +此测试类专注于分隔符的正确识别,包括逗号、冒号、分号等,验证它们是否被正确地解析为相应的DelimiterSemanticToken。 + +\textbf{测试方法:}对不同的分隔符进行词法分析,检查是否能正确地识别和返回相应的SemanticToken类型。 + +\textbf{关键代码片段:} +\begin{lstlisting}[style=csharp] +[InlineData(",123", DelimiterType.Comma)] +[InlineData(":123", DelimiterType.Colon)] +public void SmokeTest(string input, DelimiterType type) +{ + IEnumerable tokensEnumerable = _lexer.Tokenize(new StringSourceReader(input)); + List tokens = tokensEnumerable.ToList(); + SemanticToken token = tokens[0]; + Assert.Equal(SemanticTokenType.Delimiter, token.TokenType); + DelimiterSemanticToken delimiterSemanticToken = (DelimiterSemanticToken)token; + Assert.Equal(type, delimiterSemanticToken.DelimiterType); +} +\end{lstlisting} + +此测试展示了词法分析器如何处理和识别各种分隔符,并将其准确分类为DelimiterSemanticToken。 + +\paragraph{ErrorSingleTests.cs} +此部分测试聚焦于词法分析器对不合法输入的处理,包括非法数字格式、未闭合的字符串、未知字符等错误。 + +\textbf{测试方法:}验证词法分析器在遇到错误输入时是否能正确抛出LexemeException,并准确标记错误发生的位置。 + +\textbf{关键代码片段:} +\begin{lstlisting}[style=csharp] +[InlineData("program main; var a: integer; begin a := 3#; end.", 1, 43, LexemeErrorType.IllegalNumberFormat)] +public void TestUnknownCharacterError(string pascalProgram, uint expectedLine, uint expectedCharPosition, LexemeErrorType expectedErrorType) +{ + var ex = Assert.Throws(() => _lexer.Tokenize(new StringSourceReader(pascalProgram)).ToList()); + _testOutputHelper.WriteLine(ex.ToString()); + Assert.Equal(expectedErrorType, ex.ErrorType); + Assert.Equal(expectedLine, ex.Line); + Assert.Equal(expectedCharPosition, ex.CharPosition); +} +\end{lstlisting} +此测试检验了词法分析器在处理输入错误时的反应能力,包括正确抛出异常和提供错误发生的具体位置。 + + +\paragraph{IdentifierTypeTests.cs} +此测试类用于检验标识符的正确识别,确保词法分析器能准确处理包含各种字符组合的标识符。 + +\textbf{测试方法:}通过ILexer接口的Tokenize方法处理不同样式的标识符输入,验证返回的SemanticToken是否正确标识为标识符类型。 + +\textbf{关键代码片段:} +\begin{lstlisting}[style=csharp] +[InlineData("identifier", true)] +[InlineData("_identifier", true)] +[InlineData("IdentifierWithCamelCase", true)] +public void TestParseIdentifier(string input, bool expectedResult) +{ + IEnumerable tokensEnumerable = _lexer.Tokenize(new StringSourceReader(input)); + List tokens = tokensEnumerable.ToList(); + Assert.Equal(expectedResult, tokens.FirstOrDefault()?.TokenType == SemanticTokenType.Identifier); +} +\end{lstlisting} +此测试确保了各种格式的标识符能被词法分析器正确识别。 + +\paragraph{KeywordTypeTests.cs} +此测试类用于验证关键字的正确识别,确保词法分析器可以正确区分和标识Pascal中的关键字。 + +\textbf{测试方法:}对一系列预定义的关键字进行解析,检查是否能被正确地标记为关键字类型的SemanticToken。 + +\textbf{关键代码片段:} +\begin{lstlisting}[style=csharp] +[InlineData("program", KeywordType.Program)] +[InlineData("var", KeywordType.Var)] +[InlineData("begin", KeywordType.Begin)] +public void SmokeTest(string input, KeywordType type) +{ + IEnumerable tokensEnumerable = _lexer.Tokenize(new StringSourceReader(input)); + List tokens = tokensEnumerable.ToList(); + SemanticToken token = tokens[0]; + Assert.Equal(SemanticTokenType.Keyword, token.TokenType); + KeywordSemanticToken keywordSemanticToken = (KeywordSemanticToken)token; + Assert.Equal(type, keywordSemanticToken.KeywordType); +} +\end{lstlisting} +此测试展示了关键字如何被准确地解析和识别。 + +\paragraph{LexicalFileTests.cs} +此测试类旨在通过较大的文本输入来验证词法分析器的综合能力,包括正确处理变量声明、赋值、控制流等语句。 + +\textbf{测试方法:}通过解析包含多个Pascal语法结构的完整程序,验证返回的Token序列是否与预期一致。 + +\textbf{关键代码片段:} +\begin{lstlisting}[style=csharp] + [Fact] + public void TestLexicalAnalysisFirst() + { + string pascalProgram = """ + program HelloWorld; + var + message: char; + begin + message := 'h'; + writeln(message); + end. + """; + + IEnumerable tokens = _lexer.Tokenize(new StringSourceReader(pascalProgram)); + ValidateSemanticTokens(tokens, [ + SemanticTokenType.Keyword, + SemanticTokenType.Identifier, + SemanticTokenType.Delimiter, + SemanticTokenType.Keyword, + SemanticTokenType.Identifier, + SemanticTokenType.Delimiter, + SemanticTokenType.Keyword, + SemanticTokenType.Delimiter, + SemanticTokenType.Keyword, + SemanticTokenType.Identifier, + SemanticTokenType.Operator, + SemanticTokenType.Character, + SemanticTokenType.Delimiter, + SemanticTokenType.Identifier, + SemanticTokenType.Delimiter, + SemanticTokenType.Identifier, + SemanticTokenType.Delimiter, + SemanticTokenType.Delimiter, + SemanticTokenType.Keyword, + SemanticTokenType.Delimiter + ]); + } +\end{lstlisting} +通过此类测试,可以系统地验证词法分析器在处理实际Pascal程序时的表现。 + +\paragraph{NumberTests.cs} +此测试类用于验证数字的正确识别,包括整数和实数,并检查数值是否正确解析。 + +\textbf{测试方法:}对各种格式的数字字符串进行解析,检验返回的SemanticToken是否正确地反映了输入的数值类型和值。 + +\textbf{关键代码片段:} +\begin{lstlisting}[style=csharp] +[InlineData("123", 123)] +[InlineData("1.23", 1.23)] +[InlineData("1e7", 1e7)] +public void IntegerTokenTest(string input, double result) +{ + IEnumerable tokens = _lexer.Tokenize(new StringSourceReader(input)); + NumberSemanticToken token = tokens.First().Convert(); + Assert.Equal(NumberType.Real, token.NumberType); + Assert.Equal(result, token.ParseAsReal()); +} +\end{lstlisting} +此测试检验了词法分析器对不同类型数字的处理能力,确保可以正确识别和解析整数和实数。 + +\paragraph{OperatorTypeTests.cs} +此测试类旨在验证运算符的正确识别,包括加、减、乘、除等基本运算符,以及赋值和比较运算符。 + +\textbf{测试方法:}检查词法分析器是否能准确识别并返回表示特定运算符的SemanticToken。 + +\textbf{关键代码片段:} +\begin{lstlisting}[style=csharp] +[InlineData("+123", OperatorType.Plus, true)] +[InlineData("-123", OperatorType.Minus, true)] +[InlineData(":=123", OperatorType.Assign, true)] +public void ParseTest(string input, OperatorType result, bool expectedResult) +{ + IEnumerable tokensEnumerable = _lexer.Tokenize(new StringSourceReader(input)); + List tokens = tokensEnumerable.ToList(); + SemanticToken token = tokens[0]; + Assert.Equal(SemanticTokenType.Operator, token.TokenType); + OperatorSemanticToken operatorSemanticToken = (OperatorSemanticToken)token; + Assert.Equal(result, operatorSemanticToken.OperatorType); +} +\end{lstlisting} +通过这些测试可以确保各种运算符被正确识别,并正确地分类为相应的SemanticTokenType。 + +\subsubsection{语法分析器测试} + +语法分析器的测试主要分为三个部分: +\begin{itemize} + \item 针对终结符和非终结符类的测试,确保这两个类能正确地代表各种语法元素。 + \item 针对分析语法并构建LR(1)分析表的测试,确保能够正确地构建LR(1)分析表并得到语法树。 + \item 针对构建好的Pascal-S分析表进行测试,确保能够正确分析输入的语法字符串并得到语法树,亦或者识别出程序中的语法错误。 +\end{itemize} + +\paragraph{终结符和非终结符测试} +\texttt{TerminatorInnerTest} 此测试验证了不同类型的终结符之间的比较逻辑,确保语法分析器可以区分不同的词汇元素。 +\begin{lstlisting}[style=csharp] +Terminator keywordTerminator1 = new(KeywordType.Array); +Terminator keywordTerminator2 = new(KeywordType.Begin); + +Assert.False(keywordTerminator1 == keywordTerminator2); +Assert.False(keywordTerminator1 == Terminator.CharacterTerminator); +Assert.False(keywordTerminator2 == Terminator.IdentifierTerminator); + +Terminator keywordTerminator3 = new(KeywordType.Array); +Assert.Equal(keywordTerminator1, keywordTerminator3); + +Terminator delimiterTerminator1 = new(DelimiterType.Colon); +Assert.NotEqual(keywordTerminator1, delimiterTerminator1); +\end{lstlisting} + + +\texttt{TerminatorAndKeywordSemanticTokenTest} 这一测试检查终结符和关键词类型词法记号是否可以被正确匹配。 +\begin{lstlisting}[style=csharp] +Terminator keywordTerminator = new(KeywordType.Array); +KeywordSemanticToken keywordSemanticToken = new() +{ + LinePos = 0, CharacterPos = 0, KeywordType = KeywordType.Array, LiteralValue = "array" +}; +Assert.True(keywordTerminator == keywordSemanticToken); +\end{lstlisting} + +\texttt{TerminatorAndDelimiterSemanticTokenTest} 这一测试检查终结符和分隔符类型词法记号是否可以被正确匹配。 +\begin{lstlisting}[style=csharp] +Terminator terminator = new(DelimiterType.Period); +DelimiterSemanticToken token = new() +{ + LinePos = 0, CharacterPos = 0, DelimiterType = DelimiterType.Period, LiteralValue = "." +}; +Assert.True(token == terminator); +\end{lstlisting} + +\texttt{TerminatorAndOperatorSemanticTokenTest} 这一测试检查终结符和操作符类型词法记号是否可以被正确匹配。 +\begin{lstlisting}[style=csharp] +Terminator terminator = new(OperatorType.GreaterEqual); +OperatorSemanticToken token = new() +{ + LinePos = 0, CharacterPos = 0, OperatorType = OperatorType.GreaterEqual, LiteralValue = ">=" +}; +Assert.True(token == terminator); +\end{lstlisting} + +\paragraph{不含空产生式的简单语法测试} + +使用如下不含空产生式的简单语法对构建LR(1)分析自动机的过程进行测试。 + +\begin{lstlisting}[ + style=grammar, + caption={语法1} +] +ProgramStart -> ProgramStruct +ProgramStruct -> ProgramStruct+ProgramBody | ProgramStruct-ProgramBody | ProgramBody +ProgramBody -> ProgramBody*StatementList | ProgramBody/StatementList | StatementList +StatementList -> (ProgramStruct) | identifier +\end{lstlisting} + +\texttt{FirsetSetTest} 测试程序针对该语法构建的非终结符First集合是否是正确的,手动构建的First集合见表\ref{tab:grammar_1_first}。 + +\begin{lstlisting}[style=csharp] +Assert.Contains(builder.FirstSet, pair => + pair.Key == new NonTerminator(NonTerminatorType.StartNonTerminator)); +Assert.Contains(builder.FirstSet, pair => + pair.Key == new NonTerminator(NonTerminatorType.ProgramStruct)); +Assert.Contains(builder.FirstSet, pair => + pair.Key == new NonTerminator(NonTerminatorType.ProgramBody)); +Assert.Contains(builder.FirstSet, pair => + pair.Key == new NonTerminator(NonTerminatorType.StatementList)); + +foreach (HashSet terminators in builder.FirstSet.Values) +{ + Assert.Contains(Terminator.IdentifierTerminator, terminators); + Assert.Contains(new Terminator(DelimiterType.LeftParenthesis), terminators); +} +\end{lstlisting} + +\begin{table}[h] + \centering + \begin{tabular}{|c|p{10cm}|} + \hline + \textbf{非终结符} & \textbf{First集合} \\ + \hline + ProgramStruct & ( n \\ + \hline + ProgramBody & ( n \\ + \hline + StatementList & ( n \\ + \hline + \end{tabular} + \caption{语法1的First集合} + \label{tab:grammar_1_first} +\end{table} + +\texttt{StatesTest} 针对构建自动机中的状态进行测试。 + +\begin{lstlisting}[style=csharp] +Assert.Equal(30, builder.Automation.Count); + +Assert.Contains(new NonTerminator(NonTerminatorType.ProgramStruct), + grammar.BeginState.Transformer.Keys); +Assert.Contains(new NonTerminator(NonTerminatorType.ProgramBody), + grammar.BeginState.Transformer.Keys); +Assert.Contains(new NonTerminator(NonTerminatorType.StatementList), + grammar.BeginState.Transformer.Keys); +Assert.Contains(new Terminator(DelimiterType.LeftParenthesis), grammar.BeginState.Transformer.Keys); +Assert.Contains(Terminator.IdentifierTerminator, + grammar.BeginState.Transformer.Keys); +\end{lstlisting} +\clearpage +使用上述构建的分析表对\texttt{n+n}句子进行分析。 + +\begin{figure}[H] + \centering + \includegraphics[width=0.8\textwidth]{assets/测试/n+n语法树.jpg} + \caption{\texttt{n+n}的语法树} + \label{fig:grammar_1_tree_1} +\end{figure} +\clearpage +使用上述构建的分析表对\texttt{(n+n)*n}句子进行分析。 + +\begin{figure}[H] + \centering + \includegraphics[width=0.7\linewidth]{assets/测试/(n+n)n语法树.jpg} + \caption{\texttt{(n+n)*n}的语法树} + \label{fig:grammar_1_tree_2} +\end{figure} + + +\paragraph{含有空产生式的简单语法测试} + +使用含有空产生式的简单语法对构建LR(1)自动机的逻辑进行测试。使用该语法主要测试两个功能:(1) 程序能否正确的处理含有空产生式的语法,包括构建First集合、自动机等部分;(2) 该语法中含有一个移进-归约冲突,使用该语法也可以验证程序是否能够正确识别出该冲突。 + +\begin{lstlisting}[style=grammar] +Start -> ProgramStruct +ProgramStruct -> ProgramBody ProgramStruct | $\epsilon$ +ProgramBody -> identifier ProgramBody | identifier +\end{lstlisting} + +\texttt{FirstSetTest} 测试程序构建的First集合是否正确,其中手动构建的结果如表\ref{tab:grammar_2_first}所示,测试程序如下所示: + +\begin{lstlisting}[style=csharp] +Assert.Contains(builder.FirstSet, pair => +{ + if (pair.Key == new NonTerminator(NonTerminatorType.StartNonTerminator)) + { + Assert.Equal(2, pair.Value.Count); + Assert.Contains(Terminator.IdentifierTerminator, pair.Value); + Assert.Contains(Terminator.EmptyTerminator, pair.Value); + return true; + } + + return false; +}); + +Assert.Contains(builder.FirstSet, pair => +{ + if (pair.Key == new NonTerminator(NonTerminatorType.ProgramStruct)) + { + Assert.Equal(2, pair.Value.Count); + Assert.Contains(Terminator.IdentifierTerminator, pair.Value); + Assert.Contains(Terminator.EmptyTerminator, pair.Value); + return true; + } + + return true; +}); + +Assert.Contains(builder.FirstSet, pair => +{ + if (pair.Key == new NonTerminator(NonTerminatorType.ProgramBody)) + { + Assert.Single(pair.Value); + Assert.Contains(Terminator.IdentifierTerminator, pair.Value); + return true; + } + + return false; +}); +\end{lstlisting} + +\begin{table}[h] +\centering % 让表格内的文字居中 +\caption{文法2的First集} +\label{tab:grammar_2_first} +\begin{tabular}{|c|p{10cm}|} % 修改第一列为居中显示 +\hline +\textbf{文法符号} & \textbf{First集合} \\ +\hline +S & a $\epsilon$\\ +A & a $\epsilon$\\ +B & a \\ +\hline +\end{tabular} +\end{table} + +\paragraph{正常语法测试} +我们通过以下测试案例验证了编译器对Pascal程序的正确解析能力,并附上相关代码以示例: + +\texttt{无操作测试(DoNothingTest)} +\begin{lstlisting}[style=csharp] +const string program = """ + program DoNothing; + begin + end. + """; +ProgramStruct root = CompilerHelpers.Analyse(program); +Assert.Equal("DoNothing", root.Head.ProgramName.LiteralValue); +Assert.Equal(15, root.Count()); +\end{lstlisting} +验证编译器是否能正确解析一个空的Pascal程序。此测试通过,证明编译器可以正确识别和处理无操作的情况。 + +\texttt{加法测试(AddTest)} +\begin{lstlisting}[style=csharp] +const string program = """ + program Add; + var a : Integer; + begin + a := 1 + 1 + end. + """; +ProgramStruct root = CompilerHelpers.Analyse(program); +Assert.Equal("Add", root.Head.ProgramName.LiteralValue); +\end{lstlisting} +测试了基本的算术运算,确保编译器可以解析并执行简单的加法表达式。 + +\texttt{输出测试(WriteLnTest)} +\begin{lstlisting}[style=csharp] +const string program = """ + program exFunction; + const str = 'a'; + var a, b : Integer; + begin + writeln( str, ret ); + end. + """; +ProgramStruct root = CompilerHelpers.Analyse(program); +Assert.Equal("exFunction", root.Head.ProgramName.LiteralValue); +\end{lstlisting} +验证了编译器是否能处理包含输出语句的程序。通过正确解析 \texttt{writeln} 语句,测试确认了编译器的I/O语句解析能力。 + +\texttt{数组和过程测试} +包括了数组声明和使用,以及过程的定义和调用的测试(如 \texttt{ArrayTest}, \texttt{MultiplyArrayTest}, \texttt{ProcedureTest})。这些测试验证了编译器在处理复杂数据结构和程序结构时的准确性。 + +\texttt{控制流测试} +通过 \texttt{ForLoopTest} 和 \texttt{IfConditionTest} 等测试,检验了编译器对循环和条件判断的处理。确保编译器可以正确解析和执行这些控制流结构。 + +\paragraph{异常语法测试} +在这一部分,我们测试了编译器在面对不符合语法规则的Pascal代码时的错误处理能力。以下是一些关键的测试案例和对应的代码: + +\texttt{结构测试(StructTest)} +\begin{lstlisting}[style=csharp] +const string program = """ + program main; + begin + end + """; +CatchException(program); +\end{lstlisting} +测试了最基本的程序结构,确认当程序体为空时,编译器是否能正确无返回。此测试通过,说明编译器可以处理空的程序主体。 + +\texttt{赋值测试(AssignTest)} +\begin{lstlisting}[style=csharp] +const string program = """ + program main; + begin + a := 'a'; + end. + """; +CatchException(program); +\end{lstlisting} +尝试在程序中对字符进行赋值,这在Pascal语言中通常是非法的。测试验证了编译器能够抛出并捕获预期的语法错误。 + +\texttt{语句测试(StatementTest)} +\begin{lstlisting}[style=csharp] +const string program = """ + program main; + begin + if a = 1 then + doSomething; + else + doSomething; + end. + """; +CatchException(program); +\end{lstlisting} +检测了编译器对复杂语句的处理,如条件语句内部缺失正确的语句块界定。此测试确保编译器能识别并报告不完整的条件语句。 + +\texttt{循环测试(ForTest)} +\begin{lstlisting}[style=csharp] +const string program = """ + program main; + begin + for a = 1 to 100 do + doSomething + end. + """; +CatchException(program); +\end{lstlisting} +在此测试中,\texttt{for} 循环缺失了结束分号,编译器需要能识别这一点并生成错误。这证明了编译器在解析循环结构时的准确性。 + +每个测试都通过引发和捕获 \texttt{GrammarException} 来验证错误处理机制的有效性。 + +\subsubsection{生成语法分析表测试} + +本部分中主要验证内存中的LR(1)分析表和源代码形式的分析表是否一致。 + +\texttt{ConsistencyTests} 该测试通过遍历内存中的分析表和源代码形式的分析表进行比较,确保其的每一个状态和其迁移到的状态都是一致的。 + + + +\subsubsection{语义分析} + +语义分析的测试主要包含了类型检查、符号表生成、事件机制触发等方面。 + +\paragraph{类型检查} +\subparagraph{TypeCheckVisitorTests.cs} +该测试主要通过对不同类型的变量参数进行单独测试以及混合测试来验证类型检查的健壮性。如:常量类型,单个变量类型,多个变量类型,数组类型,过程参数类型,过程中的变量参数类型等。 + +\begin{itemize} + \item \textbf{ConstTypeTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + const a = 1; b = 1.23; c = 'a'; + begin + end. + """; + \end{lstlisting} + +检查常量声明类型是否与符号表中类型一致。 + + \item \textbf{SingleTypeTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + var a : integer; b : char; c : boolean; d : real; + begin + end. + """; + \end{lstlisting} + +检查变量声明类型是否与是否与符号表中类型一致。 + + \item \textbf{MulitpleTypeTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + var a, b, c, d : integer; + e, f, g : boolean; + begin + end. + """; + \end{lstlisting} + +检查多个同一类型的变量声明是否与符号表中类型一致。 + + \item \textbf{ArrayTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + var a : array [0..10] of integer; + b : array [0..10, 0..20] of integer; + c : array [100..200, 1..5] of real; + begin + end. + """; + \end{lstlisting} + +检查多个数组变量声明是否与符号表中类型一致,包括名字、数组元素类型和数组范围大小。 + + \item \textbf{ProcedureParameterTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + procedure test(a, b, c : integer); + begin + end; + begin + end. + """; + \end{lstlisting} + +检查过程声明与声明变量是否与符号表中类型一致。 + + \item \textbf{ProcedureVarParameterTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + procedure test(var a, b, c : real); + begin + end; + begin + end. + """; + \end{lstlisting} + +检查过程声明与声明引用变量是否与符号表中类型一致。 + + \item \textbf{ProcedureBothParameterTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + procedure test(a, b : integer; var c, d: char); + begin + end; + begin + end. + """; + \end{lstlisting} + +检查同时含有引用变量与传值变量的过程声明是否与符号表中类型一致。 + + \item \textbf{FunctionBothParameterTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + function test(a, b : integer; var c, d: char) : real; + begin + end; + begin + end. + """; + \end{lstlisting} + +检查同时含有引用变量与传值变量的函数声明是否与符号表中类型一致。 + + \item \textbf{ProcedureAndFunctionTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + procedure test1(a : integer; var b, c : real; d: boolean); + begin + end; + function test2(var a, b : boolean) : boolean; + begin + end; + begin + end. + """; + \end{lstlisting} + +检查同时含有引用变量与传值变量的函数与过程声明是否与符号表中类型一致。 + + \item \textbf{SubprogramSymbolTableTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + const a = 3; + var b, c : real; + procedure test(a : boolean); + var c, d : char; + begin + end; + begin + end. + """; + \end{lstlisting} + +检查子过程符号表与父符号表中存储符号的类型是否正确。 + + \item \textbf{VarAssignStatementTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + var + a : char; + b : integer; + begin + b := 3; + a := b; + end. + """; + \end{lstlisting} + +检测能否检查变量赋值表达式两侧符号类型是否一致。这个样例应当为False,因为a、b类型不一致。 + + \item \textbf{TryConstAssignStatementTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + const + a = 3; + var + b : integer; + begin + a := 4; + end. + """; + \end{lstlisting} + +检测能否检查检查常量被重新赋值。 + + \item \textbf{FunctionAssignStatementTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program exFunction; + var + a, b, ret : integer; + function max(num1, num2: integer): integer; + var + error:char; + result: integer; + begin + if (num1 > num2) then + result := num1 + else + result := num2; + max := error; + end; + begin + a := 100; + b := 200; + (* calling a function to get max value *) + ret := max(a, b); + end. + """; + \end{lstlisting} + +检测能否检查到函数实际返回值与声明函数返回值类型不一致。 + + \item \textbf{IfStatementTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program exFunction; + var + a, b, ret : integer; + begin + a := 100; + b := 200; + if 200 then + begin + b := 100; + end + else + b:=200; + end. + """; + \end{lstlisting} + +检测能否检查到if条件表达式类型必须为boolean。 + + \item \textbf{ForStatementTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program exFunction; + var + a, b, ret : integer; + c : char; + begin + for a := c to b do + begin + b:=b+10; + end; + end. + """; + \end{lstlisting} + +检测能否检查到for循环语句起始和结束参数类型必须为integer。 + + \item \textbf{ProcedureCallTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + var + a, b, c, min: integer; + error:char; + procedure findMin(x, y, z: integer; var m: integer); + begin + end; + begin + findmin(a, b, c,error); + (* Procedure call *) + end. + """; + \end{lstlisting} + +检测能否检查到调用过程时传参必须与声明时类型一致。 + +此处还检查了调用函数时传入变量到引用变量的转换是否正确。 + + \item \textbf{RecursionProcedureCallTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + var a, b:integer; c:real; + function Test0(var a1:integer; b1:integer; c1:real):integer; + begin + test0(a1,b1,c1+0.5); + end; + function Test1(var a1:integer; b1:integer; c1:real):integer; + begin + test0(1,1,1.0); + end; + begin + teSt1(a,b,1.02); + test(a, b, c); + end. + """; + \end{lstlisting} + +检测能否检查在函数中调用函数,或进行函数递归的情形。 + +同时,检测符号是否大小写不敏感。 + +这个样例将检查到test函数不存在而置为False。 + + \item \textbf{ArrayAssignIndexTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + var a : array [0..10, 0..10] of integer; + function test(a, b : integer) : integer; + begin + test := 1; + end; + begin + a[0, 1.5] := 1; + end. + """; + \end{lstlisting} + +检测能否检查为数组元素赋值时,下标不为integer的情形。 + + \item \textbf{ArrayAssignDimensionTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + var a : array [0..10, 0..10] of integer; + begin + a[0,1,3] := 1; + end. + """; + \end{lstlisting} + +检测能否检查为数组元素赋值时,元素维度错误的情形。 + + \item \textbf{ArrayAssignTypeTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + var + a : array [0..10, 0..10] of integer; + b : array [0..10, 0..20] of integer; + c : integer; + d : char; + begin + a[0,1] := c; + c := b[0,5]; + a[0,1] := b; + end. + """; + \end{lstlisting} + +检测能否检查为数组元素赋值时,表达式两侧类型不一致的情形。 + + \item \textbf{ArrayCalculationTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + var a: array[9..12, 3..5, 6..20] of real; + b: array[0..10] of integer; + begin + a[9, 4, 20] := 3.6 + b[5]; + end. + """; + \end{lstlisting} + +检测能否检查为数组元素赋值时,表达式右侧为复杂表达式时两侧类型不一致的情形。 + +同时,还检测了integer与real类型做运算时,integer类型能否被正确转换为real类型。 + + \item \textbf{BooleanOperatorTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + var flag, tag : boolean; + error:integer; + begin + tag := flag or tag; + flag := flag and error; + end. + """; + \end{lstlisting} + +检测能否检查到变量进行布尔关系运算时,关系符两侧变量不为boolean的情形。 + + \item \textbf{TrueFalseTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + var a : boolean; + begin + a := true; + a := false; + end. + """; + \end{lstlisting} + +检测能否检查到为boolean类型变量赋值时的类型问题。 + + \item \textbf{NotTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + var a: integer; + begin + a := 60; + write(not a); + end. + """; + \end{lstlisting} + +检测能否检查到not操作符参与运算时可能出现的问题。 + +不要求not右侧变量类型一定为boolean,因为not本质是将位取反。 + + \item \textbf{PascalFunctionTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + var a : integer; + begin + write(a); + read(a); + writeln(a); + end. + """; + \end{lstlisting} + +检查使用函数库的函数能否被正确调用。 + + \item \textbf{FunctionCalculateTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + var a : integer; + function test : integer; + begin + test := 1; + end; + begin + a := a + test; + end. + """; + \end{lstlisting} + +检测能否检查到无参函数返回值在表达式中使用时产生的类型问题。 + + \item \textbf{FunctionParameterCalculationTest} + \begin{lstlisting}[style=csharp] +const string program = """ + program main; + var a : integer; + function test (p : integer) : integer; + begin + test := p; + end; + begin + a := 1 + test(1); + end. + """; + \end{lstlisting} + +检测能否检查到带参函数返回值在表达式中使用时产生的类型问题。 + +\end{itemize} + + +\paragraph{事件机制触发} + +\subparagraph{ConstValueTests.cs} +该测试由事件驱动,检查在访问常量值节点时,能否正确触发事件。 + +\begin{lstlisting} + const string program = """ + program main; + const a = 1; + begin + end. + """; +\end{lstlisting} + + + +\paragraph{符号表生成} +\subparagraph{PascalTypeTests.cs} +该测试主要检查了对于复杂类型数组和函数的符号识别与加入符号表的过程正确性。 + +\begin{lstlisting}[style=csharp] + [Fact] + public void PascalArrayTypeTests() + { + PascalType array1 = new PascalArrayType(PascalBasicType.Integer, 0, 10); + PascalType array2 = new PascalArrayType(PascalBasicType.Integer, 0, 10); + + Assert.Equal(array1, array2); + } + + [Fact] + public void PascalFunctionTypeTests() + { + PascalType function1 = new PascalFunctionType([new PascalParameterType(PascalBasicType.Integer, false, "a")], + PascalBasicType.Void); + PascalType function2 = new PascalFunctionType([new PascalParameterType(PascalBasicType.Integer, false, "a")], + PascalBasicType.Void); + + Assert.Equal(function1, function2); + } +\end{lstlisting} + +\subparagraph{SymbolTableTests.cs} +该测试检查了符号表能否正确获取基本类型,能否正确插入和查找符号。对于作用域规则,在符号表存在嵌套情况时,向子符号表插入符号能否在子符号表和父符号表中查找来验证符号表的生成。 + +\begin{lstlisting}[style =csharp] + [Fact] + public void NestedTableInsertAndFindTest() + { + SymbolTable table = new(); + + Assert.True(table.TryAddSymbol(new Symbol { SymbolName = "a", SymbolType = PascalBasicType.Integer })); + Assert.True(table.TryAddSymbol(new Symbol + { + SymbolName = "temperature", SymbolType = PascalBasicType.Real, Const = true + })); + + SymbolTable child = table.CreateChildTable(); + + Assert.True(child.TryAddSymbol(new + } +\end{lstlisting} + +\paragraph{与其他部分的协同合作} + +\subparagraph{SyntaxTreeTravellerTests.cs} +该测试实现的是语义分析和语法分析的协同,对于已经创建好的语法树,语义分析能否正确工作。测试通过遍历语法树并得到正确结果验证了语义分析的协同功能。 + +\begin{lstlisting}[style =csharp] +const string program = """ + program main; + begin + end. + """; + + SampleSyntaxTreeVisitor visitor = new(); + IEnumerable tokens = _lexer.Tokenize(new StringSourceReader(program)); + ProgramStruct root = _grammarParser.Analyse(tokens); + + _traveller.Travel(root, visitor); + + List result = + [ + "ProgramStruct", + "ProgramHead", + "ProgramHead", + "ProgramBody", + "SubprogramDeclarations", + "SubprogramDeclarations", + "CompoundStatement", + "StatementList", + "Statement", + "Statement", + "StatementList", + "CompoundStatement", + "ProgramBody", + "ProgramStruct" + ]; + + string[] actual = visitor.ToString().Split('\n'); + + foreach ((string line, uint index) in result.WithIndex()) + { + Assert.Equal(line, actual[(int)index]); + } + } +\end{lstlisting} + +\subsection{集成测试} + +集成测试主要针对编译器的完整功能进行测试。而对完成功能进行测试最好的方式即为对比标准Pascal编译器和自行实现的编译器所编译的可执行程序运行结果的差异。因此我们编写了一个简单的Python自动化这个过程:针对输入的Pascal源程序文件和可选的程序输入,分别使用Free Pascal Compiler编译得到可执行程序、再使用自行实现的编译器和GCC编译器进行编译得到可执行程序,分别执行两个可执行程序并比对两个程序输出的结果。 + +集成测试的运行结果如图\ref{fig:integrated_test_figure}所示: + +\begin{figure} + \centering + \includegraphics[width=0.5\linewidth]{assets/集成测试示例.png} + \caption{集成测试的示例} + \label{fig:integrated_test_figure} +\end{figure} + +同时需要指出的是,我们在实践的过程中发现自行搭建的集成测试运行结果和头歌平台运行的结果不同,列举如下: +\begin{itemize} + \item 对于\texttt{real}类型数据的输出格式不同。在自行搭建的平台上输出的格式为空格+科学记数法格式,而在头歌平台上则是没有空格和保留6位小数的格式。 + \item 部分测试点存在头歌上的运行结果和本地运行的结果不同的问题。 +\end{itemize} + +\subsection{持续测试} + +在多人协作进行项目开发时,如何确保每个人编写的代码能够正常工作并且不会破坏程序其他部分的运行是一个重要的问题。因此,在本次的程序开发过程中,我们基于\href{https://gitea.com}{Gitea}提供了丰富CI/CD功能提供在每个提交新的代码和将代码合并到主线时自动运行所有的单元测试和集成测试的功能。 + +\begin{figure}[htbp] + \centering + \includegraphics[width=0.8\linewidth]{assets/持续测试示例.png} + \caption{持续测试的运行示例} + \label{fig:continous_test_example} +\end{figure} + +\end{document} diff --git a/docs/contents/requirements-analysis.tex b/docs/contents/requirements-analysis.tex new file mode 100644 index 0000000..b1ebf31 --- /dev/null +++ b/docs/contents/requirements-analysis.tex @@ -0,0 +1,118 @@ +\documentclass[../main.tex]{subfiles} + +\begin{document} +\section{需求分析} + +% 包括:数据流图、功能及数据说明等 +% 开发环境 + +\subsection{开发环境} + +在本次课程设计中,我们没有使用编译原理课程设计的传统工具:flex和bison,而是决定自行手动实现词法分析和语法分析。因此,我们在进行开发环境选型是就具有较高的灵活性,不必拘泥于C++语言。在综合了小组人员的开发经验和各个不同语言的优劣之后,我们决定选择.NET平台的C\#语言作为我们的开发语言。使用C\#语言作为开发语言所给我们带来的好处有: +\begin{itemize} + \item C\#是一门面向对象的新式类型安全语言,具有自动垃圾回收的功能。 + \item .NET平台提供了多种不同的部署方式。可以直接AOT(Ahead of time)编译到单个可执行程序,亦可以使用JIT(Just in time)编译的方式使用运行时进行运行。因此在共享同样的核心库时,我们可以提供编译到单个可执行文件的编译器程序,也可以基于.NET强大的Web开发能力提供在线编译器。 + \item C\#在全平台上提供了统一的开发和运行体验,适用于我们小组中需要兼容多个平台开发的需求。 +\end{itemize} + +此外,为了提高开发效率和代码的可维护性,我们还选用了一些辅助工具和库: +\begin{itemize} + \item \textbf{Gitea}:我们通过使用自行搭建的Gitea服务器进行版本控制,这样可以确保团队成员之间的代码同步和变更记录。 + \item \textbf{Gitea Actions}:我们依托Gitea提供的丰富持续集成、自动部署的功能,编写了一系列的自动化脚本,在每次提交新代码和合并到主线代码时运行单元测试和集成测试。 +\end{itemize} + +为了撰写开发文档和实验报告,我们利用了Overleaf和飞书的在线文档协作功能。这使得文档的共享和协作变得更加高效和便捷,尤其是在团队分布在不同地点时。 + +\subsection{功能分析} + +在需求文档中提供的Pascal-S语法基础上,我们希望我们的编译器支持如下的Pascal语法和功能: +\begin{enumerate} + \item 支持Pascal-S语法中的常见数据类型,包括整数、浮点数、字符值和布尔值。 + \item 支持Pascal-S语法中的常见流程控制语句,包括分支语句,循环语句(While循环和For循环) + \item 支持Pascal-S语法中的流程定义和函数定义 + \item 支持Pascal-S标准库中的输入输出函数(write, writeln, read) +\end{enumerate} + +基于上述语法和功能,我们基于Pascal-S语法设计了如下的Pascal语法。 + +\subsubsection{支持的Pascal语法}\label{pascal_grammar} + +\begin{lstlisting}[ + style=grammar, + caption={Pascal-S语法}, +] +ProgramStart -> ProgramStruct +ProgramStruct -> ProgramHead ; ProgramBody . +ProgramHead -> program id (IdList) | program id +ProgramBody -> ConstDeclarations + VarDeclarations + SubprogramDeclarations + CompoundStatement +IdList -> , id IdList | : Type +ConstDeclarations -> $\epsilon$ | const ConstDeclaration ; +ConstDeclaration -> id = ConstValue | ConstDeclaration ; id = ConstValue +ConstValue -> +num | -num | num | 'letter' | true | false +VarDeclarations -> | var VarDeclaration ; +VarDeclaration -> id IdList | VarDeclaration ; id IdList +Type -> BasicType | array [ Period ] of BasicType +BasicType -> integer | real | boolean | char +Period -> digits .. digits | Period , digits .. digits +SubprogramDeclarations -> $\epsilon$ | SubprogramDeclarations Subprogram ; +Subprogram -> SubprogramHead ; SubprogramBody +SubprogramHead -> procedure id FormalParameter + | function id FormalParameter : BasicType +FormalParameter -> $\epsilon$ | () | ( ParameterList ) +ParameterList -> Parameter | ParameterList ; Parameter +Parameter -> VarParameter | ValueParameter +VarParameter -> var ValueParameter +ValueParameter -> id IdList +SubprogramBody -> ConstDeclarations + VarDeclarations + CompoundStatement +CompoundStatement -> begin StatementList end +StatementList -> Statement | StatementList ; Statement +Statement -> $\epsilon$ + | Variable assignOp Expression + | ProcedureCall + | CompoundStatement + | if Expression then Statement ElsePart + | for id assignOp Expression to Expression do Statement + | while Expression do Statement +Variable -> id IdVarPart +IdVarPart -> $\epsilon$ | [ ExpressionList ] +ProcedureCall -> id | id () | id ( ExpressionList ) +ElsePart -> $\epsilon$ | else Statement +ExpressionList -> Expression | ExpressionList , Expression +Expression -> SimpleExpression | SimpleExpression RelationOperator SimpleExpression +SimpleExpression -> Term | SimpleExpression AddOperator Term +Term -> Factor | Term MultiplyOperator Factor +Factor -> num + | true + | false + | Variable + | ( Expression ) + | id () + | id ( ExpressionList ) + | not Factor + | - Factor + | + Factor +AddOperator -> + | - | or +MultiplyOperator -> * | / | div | mod | and +RelationOperator -> = | <> | < | <= | > | >= +\end{lstlisting} + +\paragraph{对语法的调整} 相较于需求中给定的Pascal-S语法,我们在开发和实践的过程中对于语法做出了如下的调整和扩充。 + +\begin{itemize} + \item 消除文法中存在的部分左递归,例如VarDeclaration。消除左递归使得我们可以使用S-属性的翻译方案进行类型检查和代码生成。 + \item 将ProcedureCall中添加空括号的产生式。支持在调用无参的过程或者是函数时添加一对空括号。 + \item 删除Statment中产生funcid的产生式。因为Pascal中的函数返回语句只是一个合法的赋值语句,在实际上并不会终止函数的执行。因此删除该产生式,并在类型检查和代码生成的阶段进行进一步的处理。 + \item 添加Factor中对于加号的支持。支持在运算的过程中使用显式注明的整数,即$ 1 ++ 1$类型的表达式。 + \item 调整对于Factor中对于ProcedureCall的定义为Id() | Id (ExpressionList)。支持调用没有参数的函数。 + \item 在FormalParameter中添加一对空括号。支持在定义无参的过程和函数时添加一对空括号。 + \item 增加while-do语句的支持。 +\end{itemize} + +\paragraph{冲突的处理} 在实现使用LR(1)分析技术的语法分析器时,我们发现在需求分析中给出的Pascal-S语法中存在着一处移进-归约冲突,即语法中的ElsePart非终结符:在对含有多个嵌套的If语句进行处理时,ElsePart既可以直接从空产生式中归约出来,也继续可以继续移进。但是在语法层面上,Else语句应该和最近的一个If语句相结合。因此,在语法分析器中做如下处理:(1) 在构建分析表出添加一个特殊判断,如果是检测到ElsePart的移进-归约冲突,则不报错继续处理;(2) 在按照分析表进行分析时,首先进行移进操作,然后再进行归约操作,这样就能保证ElsePart会优先和最近和If语句进行结合。 + +\end{document} \ No newline at end of file diff --git a/docs/contents/source.tex b/docs/contents/source.tex new file mode 100644 index 0000000..0e96b89 --- /dev/null +++ b/docs/contents/source.tex @@ -0,0 +1,256 @@ +\documentclass[../main.tex]{subfiles} + + +\begin{document} + +\section{源程序清单} + +为了使得项目开发更加清晰,程序中由五个C\#项目组成: +\begin{itemize} + \item Canon.Core 编译器的核心库,含有编译器的所有核心功能。 + \item Canon.Tests 编译器核心库的测试库,含有项目中编写的所有单元测试。 + \item Canon.Console 编译器的命令行版本程序,在核心库的基础上以命令行的方式同编译器进行交互。 + \item Canon.Server 编译器的服务器版本程序,以Web的方式同编译器进行交互。 + \item Canon.Generator 用于生成源代码形式的LR(1)分析表的工具。 +\end{itemize} + +代码中的总行数如表\ref{tab:code_lines}所示。 + +\begin{table}[htbp] + \centering + \begin{tabular}{|l|r|r|r|r|r|} + \hline + 语言 & 文件数 & 行数 & 空白行数 & 注释数 & 代码行数 \\ + \hline + C\# & 132 & 13263 & 1889 & 978 & 10396 \\ + Pascal & 95 & 4989 & 368 & 34 & 4587 \\ + TypeScript & 8 & 521 & 52 & 7 & 462 \\ + MSBuild & 6 & 195 & 23 & 2 & 170 \\ + TypeScript Typings & 2 & 149 & 7 & 13 & 129 \\ + HTML & 1 & 12 & 0 & 0 & 12 \\ + Python & 1 & 111 & 26 & 0 & 85 \\ + \hline + \end{tabular} + \caption{代码行数统计} + \label{tab:code_lines} +\end{table} + +\subsection{Canon.Core项目} + +\begin{verbatim} +. +├── Abstractions +│ ├── ICompilerLogger.cs +│   ├── IGrammarParser.cs +│   ├── ILexer.cs +│   ├── ISourceReader.cs +│   ├── ITransformer.cs +│   └── SyntaxNodeVisitor.cs +├── Canon.Core.csproj +├── CodeGenerators +│   └── CCodeBuilder.cs +├── Enums +│   ├── BasicType.cs +│   ├── ErrorEnums.cs +│   ├── GrammarEnums.cs +│   └── SemanticEnums.cs +├── Exceptions +│   ├── GrammarException.cs +│   ├── LexemeException.cs +│   ├── ReduceAndShiftConflictException.cs +│   └── ReduceConflictException.cs +├── GrammarParser +│   ├── Expression.cs +│   ├── GeneratedParser.g.cs +│   ├── GrammarBuilder.cs +│   ├── Grammar.cs +│   ├── LrState.cs +│   ├── PascalGrammar.cs +│   └── Terminator.cs +├── LexicalParser +│   ├── LexemeFactory.cs +│   ├── Lexer.cs +│   ├── LexRules.cs +│   └── SemanticToken.cs +├── SemanticParser +│   ├── CodeGeneratorVisitor.cs +│   ├── PascalArrayType.cs +│   ├── PascalBasicType.cs +│   ├── PascalFunctionType.cs +│   ├── PascalParameterType.cs +│   ├── PascalType.cs +│   ├── Symbol.cs +│   ├── SymbolTable.cs +│   ├── SyntaxTreeTraveller.cs +│   ├── TypeCheckVisitor.cs +│   └── TypeTable.cs +└── SyntaxNodes + ├── AddOperator.cs + ├── BasicType.cs + ├── CompoundStatement.cs + ├── ConstDeclaration.cs + ├── ConstDeclarations.cs + ├── ConstValue.cs + ├── ElsePart.cs + ├── Expression.cs + ├── ExpressionList.cs + ├── Factor.cs + ├── FormalParameter.cs + ├── IdentifierList.cs + ├── IdentifierVarPart.cs + ├── MultiplyOperator.cs + ├── NonTerminatedSyntaxNode.cs + ├── Parameter.cs + ├── ParameterList.cs + ├── Period.cs + ├── ProcedureCall.cs + ├── ProgramBody.cs + ├── ProgramHead.cs + ├── ProgramStruct.cs + ├── RelationOperator.cs + ├── SimpleExpression.cs + ├── Statement.cs + ├── StatementList.cs + ├── SubprogramBody.cs + ├── Subprogram.cs + ├── SubprogramDeclarations.cs + ├── SubprogramHead.cs + ├── SyntaxNodeBase.cs + ├── Term.cs + ├── TerminatedSyntaxNode.cs + ├── TypeSyntaxNode.cs + ├── ValueParameter.cs + ├── VarDeclaration.cs + ├── VarDeclarations.cs + ├── Variable.cs + └── VarParameter.cs +\end{verbatim} + +\subsection{Canon.Console项目} + +\begin{verbatim} +. +├── Canon.Console.csproj +├── Extensions +│   └── RootCommandExtensions.cs +├── Models +│   └── CompilerOption.cs +├── Program.cs +└── Services + ├── Compiler.cs + ├── CompilerLogger.cs + └── StringSourceReader.cs +\end{verbatim} + +\subsection{Canon.Server项目} + +\begin{verbatim} +. +├── appsettings.json +├── Canon.Server.csproj +├── client-app +│   ├── index.html +│   ├── package.json +│   ├── pnpm-lock.yaml +│   ├── public +│   │   └── pic +│   │   └── uncompiled.png +│   ├── src +│   │   ├── App.tsx +│   │   ├── main.tsx +│   │   ├── openapi.d.ts +│   │   ├── Pages +│   │   │   ├── HistoryPage.tsx +│   │   │   ├── Index.tsx +│   │   │   ├── InputField.tsx +│   │   │   ├── Loader.tsx +│   │   │   └── OutputField.tsx +│   │   └── vite-env.d.ts +│   ├── tsconfig.json +│   ├── tsconfig.node.json +│   └── vite.config.ts +├── Controllers +│   ├── CompilerController.cs +│   └── FileController.cs +├── DataTransferObjects +│   ├── CompileResponse.cs +│   └── SourceCode.cs +├── Dockerfile +├── Entities +│   └── CompileResult.cs +├── Extensions +│   └── ServiceCollectionExtensions.cs +├── Models +│   ├── Brush.cs +│   ├── CodeReader.cs +│   ├── CompilerLogger.cs +│   └── PresentableTreeNode.cs +├── Program.cs +├── Properties +│   └── launchSettings.json +├── Services +│   ├── CompileDbContext.cs +│   ├── CompilerService.cs +│   ├── DatabaseSetupService.cs +│   ├── GridFsService.cs +│   └── SyntaxTreePresentationService.cs +└── wwwroot +\end{verbatim} + +\subsection{Canon.Generator项目} + +\begin{verbatim} +. +├── Canon.Generator.csproj +├── Extensions +│   └── RootCommandExtension.cs +├── GrammarGenerator +│   ├── GenerateCommand.cs +│   ├── GeneratedGrammarParser.cs +│   ├── GeneratedTransformer.cs +│   └── GrammarExtensions.cs +├── Program.cs +└── SyntaxVisitorGenerator + └── SyntaxVisitorGenerator.cs +\end{verbatim} + +\subsection{Canon.Tests项目} + +\begin{verbatim} +. +├── Canon.Tests.csproj +├── GeneratedParserTests +│   └── GenerateParserTests.cs +├── GlobalUsings.cs +├── GrammarParserTests +│   ├── PascalGrammarFailedTests.cs +│   ├── PascalGrammarTests.cs +│   ├── SimpleGrammarTests.cs +│   ├── SimpleGrammarWithEmptyTests.cs +│   └── TerminatorTests.cs +├── LexicalParserTests +│   ├── CharacterTypeTests.cs +│   ├── DelimiterTests.cs +│   ├── ErrorSingleTests.cs +│   ├── IndentifierTypeTests.cs +│   ├── KeywordTypeTests.cs +│   ├── LexicalFileTests.cs +│   ├── NumberTests.cs +│   └── OperatorTypeTests.cs +├── SemanticTests +│   ├── ConstValueTests.cs +│   ├── PascalTypeTests.cs +│   ├── SymbolTableTests.cs +│   ├── SyntaxTreeTravellerTests.cs +│   ├── Tests.cs +│   └── TypeCheckVisitorTests.cs +└── Utils + ├── CompilerHelpers.cs + ├── EnumerableExtensions.cs + ├── SampleSyntaxTreeVisitor.cs + ├── StringSourceReader.cs + ├── StringSourceReaderTests.cs + └── TestLogger.cs +\end{verbatim} + +\end{document} \ No newline at end of file diff --git a/docs/contents/summary.tex b/docs/contents/summary.tex new file mode 100644 index 0000000..0f82fcf --- /dev/null +++ b/docs/contents/summary.tex @@ -0,0 +1,110 @@ +\documentclass[../main.tex]{subfiles} + +\begin{document} +\section{课程设计总结} + +% 1) 体会/收获(每个成员完成的工作、收获等) +% 2) 设计过程中遇到或存在的主要问题及解决方案 +% 3) 改进建议 + +\subsection{成员分工} + +\begin{table}[htbp] + \centering + \begin{tabular}{|l|l|} + \hline + \textbf{姓名} & \textbf{分工} \\ + \hline + 任昌骏 & 组长,主持开发工作,负责编译器的总体设计 \\ + \hline + 张弈纶 & 负责语法分析和类型检查部分的开发,前端界面的搭建 \\ + \hline + 兰建国 & 类型系统、符号表和代码生成部分的开发 \\ + \hline + 肖可扬 & 词法令牌、词法分析器的设计与实现 \\ + \hline + 杜含韵 & 词法分析和语法分析单元测试的编写 \\ + \hline + 陈劲淞 & 撰写课程设计文档 \\ + \hline + \end{tabular} + \caption{成员分工表} + \label{tab:my_label} +\end{table} + +\subsection{体会与收获} + +\subsubsection{张弈纶} + +这次课设我主要负责语法的定义引入、语义分析中代码类型检查和前端可视化落地。上学期我仅仅是完成了课程要求的词法分析和语法分析实验,对语义分析和其中的代码类型检查部分没有实际的了解。通过这次课程设计,我充分理解了代码类型检查的必要性和其实际运作流程。并且,我通过这次的编码了解了c\#访问者模式和事件机制的使用,对我来说是一次开阔眼界的过程,收获颇多。 + +编码过程中,我经历了多次代码重构和迭代更新,这让我充分认识到代码质量对编码效率的影响。同时,对于一些代码设计,也重复验证了很多次。这给我很深的体会,在今后的程序设计中要做好规划,提高代码的鲁棒性与可读性。 + +这次课设还让我培养了一定的文献调查能力。对于语法树的绘制,我参考了相关论文并进行了复现,这也让我锻炼了论文的调研能力。同时,这次课设也让我加深了团队合作的精神,培养了团结协作的能力。 + +\subsubsection{兰建国} + +本次课程设计我主要负责代码生成部分,在此过程中我学到了很多。首先我对代码生成的过程有了更深入的理解。在一开始,我以为代码生成就是机械地将Pascal-S代码翻译到C语言代码,但是在动手编码之后发现困难重重。在经过几次代码重构和迭代之后,我反应过来发现是设计方面的缺陷。在开始的设计中,我采取的是一种"遍历到哪,翻译到哪"的设计。但实际上应该在语法树上收集够了相应信息之后才进行代码的生成。其次,我体会到了三地址代码的便捷性。在引入三地址代码之前,很多语句的代码生成很难进行,在引入三地址代码之后,代码生成的过程也更加清晰,程序的可扩展性也大大增加。此外,我还感受到了在开发过程的不稳定性,在实际开发过程中,设计方案需要不断调整,以适应各种变化。最后,此次课设还让我感受到了团队交流的重要性,通过在发现问题时及时沟通,提高了我们的开发效率。 + +\subsubsection{任昌骏} + +在这次的课程设计中我有幸担任组长,负责整个编译器的总体设计。站在现在的角度上看来,当时选择整个编译器的设计和实现不依赖与传统的工具而是完全手动实现是非常冒险的,并且在语言选型方面也非常的``激进''。所幸在全组同学的通力配合以及王老师和助教同学的大力支持下顺利完成了。 + +这次的课程设计也算是我个人能力上的一次突破。在以往的项目经历中,很少有像``设计一个编译器''这样一个在算法设计和软件设计上都非常具有挑战性的课题。也正是有了这个机会,使我得以将过去几年学到的各种知识和技能融会贯通,无论是按照编译课本上的描述实现词法分析和语法分析的相关算法,还是考古论文实现树的可视化绘制都是对于我算法能力的考验;亦或是使用访问者模式在语法树节点上扩展各种功能,还是通过事件的机制抽象同一个语法树节点可能使用的多个不同的生成式,都是对我软件工程能力的挑战。而且,编译原理课程设计作为大学少数几门要求由5至7人协作完成的课程设计也进一步锻炼了我组织小组合作的能力。无论是复杂项目的\texttt{Git}管理还是使用\texttt{CI/CD}实现持续测试和快速部署,都是我在个人项目之中难以接触到的东西。 + +最后还是非常感谢老师和各位同学能给我这样一个锻炼自己和提高自己的机会,在未来我一定认真复盘这次课程设计中的得与失,进一步的提高自己的个人能力。 + +\subsubsection{肖可扬} + +这次课设我主要负责词法分析器的编写,在上学期实验的基础上,此次课设的词法分析器更接近真实场景,需要处理更多与Pascal语法特性相关的内容,所以在前期调研方面我们首先形成了翻译表,在这个过程中我感受到了明确需求的重要性。此外,这是我第一次使用C\#在纯代码环境中开发软件,和小组同学学习到了许多工具的使用、软件的组织架构以及代码编写规范等内容;在重构自己的代码的过程中,我更好地理解和掌握了面向对象的相关语法以及设计模式。同时,和负责测试的同学进行交流也是宝贵的经验,我意识到了自己在编程方面的严谨性还有待提高,需要更系统地全面地考虑输入的各种情况。在编写程序过程中,我明白了团队交流的意义,在编译的不同流程之间确定接口以及对特殊问题的配合处理需要团队紧密讨论、通力协作,才能提高代码的效率。 + +\subsubsection{杜含韵} + +本次课程设计我主要参与了单元测试的部分编写,并在此过程中受益良多。首先是通过此次项目,我熟悉了C\#语言特性与.NET框架,纠正了先前对git的错误使用,帮助拓宽了我的技术认知与技术组成。其次是对Xunit测试框架的熟练使用也使得我在本学期其他课程中操作实践。最后是作为对上学期编译原理理论课程延申而出的课程设计,帮助我学会如何将理论转化为实践,以及如何克服实践的具体困难。同时,我还认识到了测试的编写需要更为明确的对组件任务的了解和认知,需要从宏观角度上思考测试的方向和方法,组内同学的实践也让我了解到测试载体的多样化。而头歌平台测试集对边界情况的探索也帮助我认识到在思考的完善性上有着诸多不足。最后,我最为感激的是组内同学的通力配合和辛苦付出,他们在我遇到困难时的耐心解答与帮助使得我受益匪浅。此次学习实践经验也将助力我日后的学习生活行稳致远。 + +\subsubsection{陈劲淞} + +在本次课程设计中,我主要负责撰写项目的文档。这不仅仅包括项目的设计文档,还有整个开发过程的文档记录和最终的报告。通过这个过程,我深刻理解到了文档在软件开发过程中的重要性。良好的文档不仅可以帮助团队成员理解和维护代码,还可以为未来的开发提供参考。 + +首先,我学习并实践了如何使用\LaTeX 来创建专业的文档。这包括了解其基本语法、文档结构组织、图表和代码的插入等。这些技能的获得,让我在未来的学术写作和报告制作中更加得心应手。 + +其次,团队在开发过程中采用Docker容器化技术,这极大地提高了开发环境的一致性和项目的可移植性。通过Docker,我们能够确保每个团队成员都在相同的环境中开发和测试,减少了环境差异带来的问题。我在文档中详细记录了如何使用Docker来配置和管理我们的开发环境,这对于团队成员理解整个系统的部署和运行至关重要。 + +同时,本项目中使用Git进行版本控制和团队协作,我负责记录各个分支的合并和版本发布的详细过程,确保所有团队成员都能迅速地获取最新的项目状态和历史修改记录。这不仅提高了团队的工作效率,也增强了项目的可追溯性。 + +在编译原理中,词法分析、语法分析和语义分析是构建编译器的重要步骤。在本次课程设计中,我们团队的工作涉及到了这些方面,我作为文档撰写者也深刻地参与其中并从中受益匪浅。通过撰写文档的过程,我加深了对整个项目的理解,提高了与团队成员的交流合作能力,并锻炼了自己的表达和文字组织能力。 + +此外,我参与了单元测试和集成测试的文档撰写,记录了测试策略和测试结果。通过Xunit框架进行单元测试,以及使用Jenkins进行持续集成,我们能够及时发现并解决开发过程中出现的问题,保证软件质量。这一过程不仅加深了我对测试理论的理解,也提升了我在实际项目中应用测试的能力。 + +最后,通过这次经验,我认识到了持续学习和自我提升的重要性。未来,我希望能继续提高我的专业技能,尤其是在技术写作和项目管理方面,以便在未来的职业生涯中更好地服务于团队和项目。 + +\subsection{设计中的主要问题和解决方案} + +\paragraph{生成LR(1)分析表耗时较长} + 在编译的过程中,从原始的语法生成对应的LR(1)分析表是一个时间复杂度较大的工作。经过实际测试,生成本课程设计中需要支持的基础语法对应的分析表就需要大约7秒至10秒的时间。 + +\textbf{解决方案}: 将生成好的LR(1)分析表以C\#源代码的形式直接输出,再打包编译到程序中。在输入的Pasccal语法没有变化的情况下不用重复的生成相同的分析表。 + +\paragraph{语法树的访问者和类型检测访问者} +在编译过程中,管理和遍历语法树对于进行有效的类型检查和语义分析至关重要。传统的遍历方法可能导致代码重复,难以维护,且使用递归进行遍历还可能因为递归深度过深而造成占空间耗尽的运行时错误。 + +\textbf{解决方案}: 采用访问者设计模式(Visitor Pattern)来分离数据结构和操作。这使得在不修改语法树结构的情况下,添加新的操作变得简单,提高了代码的可维护性和扩展性。对于类型检测,定义一个专门的类型检测访问者,该访问者遍历语法树并对每个节点进行类型验证。 + +\paragraph{语法中的左递归难以进行类型检查} +在原始需求文档中给定的Pascal语法中存在的大量左递归,使得我们在进行语义分析时很难设计出S-属性的翻译方案。 + +\textbf{解决方案}: 改写文法,消除文法中的左递归,详解\ref{pascal_grammar}节中给出的对应语法和修改说明。 + +\paragraph{代码生成中涉及的各种困难} +在初始设计面向C语言的代码生成时,在翻译循环语句和函数调用语句时遇到了很大的困难,因为初始化仍然采用一对一的翻译思想,试图将Pascal中的每一个语法结构都翻译到一个对应的语法结构。 + +\textbf{解决方案}: 借鉴三地址代码,将翻译思想设计为翻译到一种使用C语言书写的三地址代码,并且大量的使用\texttt{goto}语句和标签,成功地解决了上述问题。 + +\subsection{改进建议} + +\paragraph{提供更为详尽的报错信息} 目前语法分析的报错系统仍然十分的不人性化,仅仅输出了编译器此时希望输入什么样的词法记号。 + +\paragraph{进行代码优化} 因为在进行代码生成时使用率类似于三地址代码的代码生成形式,因此在进行代码生成会生成大量的冗余变量,造成程序的编译时间和运行占用的内存空间都非常大。 + + +\end{document} \ No newline at end of file diff --git a/docs/contents/tasks-and-objectives.tex b/docs/contents/tasks-and-objectives.tex new file mode 100644 index 0000000..c5d8f2d --- /dev/null +++ b/docs/contents/tasks-and-objectives.tex @@ -0,0 +1,27 @@ +\documentclass[../main.tex]{subfiles} + +\begin{document} +\section{课程设计的任务和目标} + +课程设计的目标是设计一个针对Pascal-S语言的编译程序,使用C语言作为编译器的目标语言。 + +课程设计的目标是设计并实现一个编译器,该编译器能够将Pascal-S语言编写的源代码转换为C语言代码。Pascal-S是Pascal语言的一个子集,专门用于教学目的,它包含了Pascal语言的核心特性,但去除了一些复杂的构造以简化学习和编译过程。 + +编译器的设计将分为几个主要部分: + +\begin{enumerate} + \item \textbf{词法分析器(Lexical Analyzer)}: 该部分将读取源代码,并将其分解成一系列的标记(tokens),这些标记是编译过程中语法分析的基本单位。 + \item \textbf{语法分析器(Syntax Analyzer)}: 语法分析器将使用词法分析器提供的标记来构建抽象语法树(AST)。AST是源代码的树状表示,反映了程序的结构。 + \item \textbf{语义分析器(Semantic Analyzer)}: 语义分析器将检查AST以确保源代码的逻辑是一致的,例如变量的声明与使用是否匹配,类型是否兼容等。 +\item \textbf{中间代码生成器(Intermediate Code Generator)}: 该部分将AST转换为中间表示(IR),IR是一种更接近机器语言的代码形式,但仍然保畴一定程度的抽象。 + \item \textbf{代码优化器(Code Optimizer)}: 代码优化器将对IR进行分析和转换,以提高生成的C代码的效率和性能。 + \item \textbf{目标代码生成器(Target Code Generator)}: 最后,目标代码生成器将把优化后的IR转换为C语言代码,这是编译过程的最终产物。 +\end{enumerate} + +此外,编译器还将包括错误处理机制,以便在编译过程中捕捉并报告错误,帮助用户理解并修正源代码中的问题。 + +整个编译器的设计将遵循模块化原则,每个部分都将有明确的接口和职责,以便于测试和维护。我们还将使用C语言的特性,如指针和结构体,来高效地实现编译器的各个组成部分。 + +最终,我们的目标是实现一个健壮的编译器,它不仅能够正确地将Pascal-S代码转换为C代码,而且还能够提供有用的错误信息,帮助用户改进他们的源代码。 + +\end{document} \ No newline at end of file diff --git a/docs/main.tex b/docs/main.tex new file mode 100644 index 0000000..ee96a7d --- /dev/null +++ b/docs/main.tex @@ -0,0 +1,143 @@ +\documentclass[12pt, a4paper, oneside]{ctexart} +\usepackage{amsmath, amsthm, amssymb, appendix, bm, graphicx, hyperref, mathrsfs, geometry} +\usepackage{float} +\usepackage{subcaption} +\usepackage{listings} +\usepackage{longtable} +\usepackage[dvipsnames]{xcolor} +\usepackage{subfiles} +\usepackage{fontspec} +\usepackage{array} + +\linespread{1.5} +\pagestyle{plain} +\geometry{a4paper, scale=0.8} + +% 定义书写语法时的listings style +\lstdefinestyle{grammar}{ + basicstyle=\ttfamily, + breaklines=true, + mathescape=true, + morekeywords={ProgramStart, ProgramStruct, ProgramHead, ProgramBody, IdList, ConstDeclarations, ConstDeclaration, ConstValue, VarDeclarations, VarDeclaration, Type, BasicType, BasicType, Period, SubprogramDeclarations, Subprogram, SubprogramHead, FormalParameter, ParameterList, Parameter, VarParameter, ValueParameter, SubprogramBody, CompoundStatement, StatementList, Statement, Variable, IdVarPart, ProcedureCall, ElsePart, ExpressionList, Expression, SimpleExpression, Term, Factor, AddOperator, MultiplyOperator, RelationOperator}, + keywordstyle=\bfseries\color{NavyBlue}, + emphstyle={\bfseries\color{Rhodamine}}, + emph={program, id, num, true, false,var, array, of, integer, real, boolean, char, digits, procedure, function, begin, end, assignOp, if, then, for, to ,do ,while, else, not, or, div, mod, and, const, letter} +} + +% 定义书写C#时的listings style +\lstdefinestyle{csharp}{ + language=[sharp]c, + breaklines=true, + basicstyle=\ttfamily, + keywordstyle=\bfseries\color{violet}, + emphstyle=\bfseries\color{blue}, + morekeywords={required, get, set, init}, + showstringspaces=false, +} + +% 定义书写C时的listings style +\lstdefinestyle{c}{ + language=c, + breaklines=true, + basicstyle=\ttfamily, + keywordstyle=\bfseries\color{blue}, + showstringspaces=false, +} + +% 定义书写Pascal时的listings style +\lstdefinestyle{pascal}{ + language=Pascal, + breaklines=true, + basicstyle=\ttfamily, + keywordstyle=\bfseries\color{violet}, + emphstyle=\bfseries\color{blue}, +} + +\begin{document} + +\begin{titlepage} +% 标题 +\begin{center} + \Huge{\textbf{北\quad 京\quad 邮\quad 电\quad 大\quad 学}} + + \vspace{2em} + + \Large{\textbf{《编译原理与技术课程设计》}} + + \Large{\textbf{报\qquad 告}} + + \vspace{4em} + + \large{指导教师: \underline{\makebox[8em][c]{王雅文}}} +\end{center} + +\vspace{6em} + +% 个人信息 +\begin{table}[h] + \centering + \begin{tabular}{|c|c|c|c|} + \hline + \textbf{姓名} & \textbf{班级} & \textbf{学号} & \textbf{备注} \\ + \hline + 张弈纶 & 2021211308 & 2021211177 & \\ + \hline + 兰建国 & 2021211308 & 2021211179 & \\ + \hline + 任昌骏 & 2021211308 & 2021211180 & 组长 \\ + \hline + 肖可扬 & 2021211308 & 2021211186 & \\ + \hline + 杜含韵 & 2021211308 & 2021211188 & \\ + \hline + 陈劲淞 & 2020219308 & 2018211608 & \\ + \hline + \end{tabular} +\end{table} + +% 封底 +\vspace{8em} + +\begin{center} + \Large{\textbf{计算机学院(国家示范性软件学院)}} + + \Large{2024年5月} +\end{center} +\end{titlepage} + +\clearpage + +% 目录 +% 目录的页码和正文的页码不一致 +\pagenumbering{Roman} +\setcounter{page}{1} +\tableofcontents + +\clearpage +\setcounter{page}{1} +\pagenumbering{arabic} + +\begin{center} + \Large{\textbf{Pascal-S 语言编译程序的设计与实现}} +\end{center} + +\subfile{contents/tasks-and-objectives} + +\subfile{contents/requirements-analysis} + +\subfile{contents/general-design} + +\subfile{contents/detailed-design} + +\subfile{contents/source} + +\subfile{contents/program-test} + +\subfile{contents/summary} + +\clearpage + +\bibliographystyle{unsrt} +\bibliography{ref} + +\end{document} diff --git a/docs/ref.bib b/docs/ref.bib new file mode 100644 index 0000000..439ff7f --- /dev/null +++ b/docs/ref.bib @@ -0,0 +1,20 @@ +@incollection{goos_improving_2002, + location = {Berlin, Heidelberg}, + title = {Improving Walker’s Algorithm to Run in Linear Time}, + volume = {2528}, + isbn = {978-3-540-00158-4 978-3-540-36151-0}, + url = {http://link.springer.com/10.1007/3-540-36151-0_32}, + abstract = {The algorithm of Walker [5] is widely used for drawing trees of unbounded degree, and it is widely assumed to run in linear time, as the author claims in his article. But the presented algorithm clearly needs quadratic runtime. We explain the reasons for that and present a revised algorithm that creates the same layouts in linear time.}, + pages = {344--353}, + booktitle = {Graph Drawing}, + publisher = {Springer Berlin Heidelberg}, + author = {Buchheim, Christoph and Jünger, Michael and Leipert, Sebastian}, + editor = {Goodrich, Michael T. and Kobourov, Stephen G.}, + editorb = {Goos, Gerhard and Hartmanis, Juris and Van Leeuwen, Jan}, + editorbtype = {redactor}, + urldate = {2024-04-14}, + date = {2002}, + langid = {english}, + doi = {10.1007/3-540-36151-0_32}, + note = {Series Title: Lecture Notes in Computer Science}, +}