Full Code of xiaoweiChen/Learn-LLVM-17 for AI

main c31b917c7617 cached
104 files
416.1 KB
179.5k tokens
1 requests
Download .txt
Showing preview only (687K chars total). Download the full file or copy to clipboard to get everything.
Repository: xiaoweiChen/Learn-LLVM-17
Branch: main
Commit: c31b917c7617
Files: 104
Total size: 416.1 KB

Directory structure:
gitextract_ejqkru__/

├── .gitignore
├── LICENSE
├── Learn-LLVM-17.tex
├── README.md
└── content/
    ├── chapter0/
    │   ├── 0.tex
    │   ├── 1.tex
    │   ├── 2.tex
    │   └── 3.tex
    ├── part1/
    │   ├── chapter1/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   ├── 4.tex
    │   │   └── 5.tex
    │   ├── chapter2/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   ├── 4.tex
    │   │   ├── 5.tex
    │   │   ├── 6.tex
    │   │   └── 7.tex
    │   └── part1.tex
    ├── part2/
    │   ├── chapter3/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   ├── 4.tex
    │   │   ├── 5.tex
    │   │   ├── 6.tex
    │   │   ├── 7.tex
    │   │   └── 8.tex
    │   ├── chapter4/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   └── 4.tex
    │   ├── chapter5/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   ├── 4.tex
    │   │   └── 5.tex
    │   ├── chapter6/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   └── 4.tex
    │   ├── chapter7/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   ├── 4.tex
    │   │   ├── 5.tex
    │   │   └── 6.tex
    │   └── part2.tex
    ├── part3/
    │   ├── chapter10/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   ├── 4.tex
    │   │   ├── 5.tex
    │   │   ├── 6.tex
    │   │   └── 7.tex
    │   ├── chapter8/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   ├── 4.tex
    │   │   ├── 5.tex
    │   │   └── 6.tex
    │   ├── chapter9/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   ├── 4.tex
    │   │   ├── 5.tex
    │   │   └── 6.tex
    │   └── part3.tex
    └── part4/
        ├── chapter11/
        │   ├── 0.tex
        │   ├── 1.tex
        │   ├── 2.tex
        │   ├── 3.tex
        │   ├── 4.tex
        │   ├── 5.tex
        │   ├── 6.tex
        │   ├── 7.tex
        │   └── 8.tex
        ├── chapter12/
        │   ├── 0.tex
        │   ├── 1.tex
        │   ├── 2.tex
        │   ├── 3.tex
        │   ├── 4.tex
        │   ├── 5.tex
        │   ├── 6.tex
        │   ├── 7.tex
        │   ├── 8.tex
        │   └── 9.tex
        ├── chapter13/
        │   ├── 0.tex
        │   ├── 1.tex
        │   ├── 2.tex
        │   ├── 3.tex
        │   └── 4.tex
        └── part4.tex

================================================
FILE CONTENTS
================================================

================================================
FILE: .gitignore
================================================
*.pdf
*.aux
*.log
*.out
*.gz
*toc
*.listing
*.synctex(busy)
/_minted-Learn-LLVM-17/


================================================
FILE: LICENSE
================================================
                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.


================================================
FILE: Learn-LLVM-17.tex
================================================

\documentclass[11pt,a4paper,UTF8]{book}

\usepackage{minted}
\usepackage[T1]{fontenc}
\usepackage[utf8]{inputenc}
\usepackage{authblk}

\usepackage{fontspec}                  %引入字体设置宏包
\setmainfont{Times New Roman}             %设置英文正文字体
% Courier New
% Book Antique
\setsansfont{Arial}                    %英文无衬线字体
\setmonofont{Courier New}              %英文等宽字体

\usepackage{ctex} %导入中文包
%\usepackage{ulem}
\usepackage{tocvsec2}
\usepackage{verbatim}

\usepackage{tabularx}
\usepackage{longtable}
\usepackage{booktabs}
\usepackage{multirow}
\usepackage{bbding}
\usepackage{float}
\usepackage{xspace}
\usepackage[none]{hyphenat}

\usepackage{graphicx}
\usepackage{subfigure}
\usepackage{pifont}

\usepackage{hyperref}  %制作pdf的目录
\usepackage{subfiles} %使用多文件方式进行

\usepackage{geometry} %设置页边距的包
\geometry{left=2.5cm,right=2cm,top=2.54cm,bottom=2.54cm} %设置书籍的页边距

\usepackage{url}
\hypersetup{hidelinks, %去红框
  colorlinks=true,
  allcolors=black,
  pdfstartview=Fit,
  breaklinks=true
}

% 调整itemlist中的行间距
\usepackage{enumitem}
\setenumerate[1]{itemsep=0pt,partopsep=0pt,parsep=\parskip,topsep=5pt}
\setitemize[1]{itemsep=0pt,partopsep=0pt,parsep=\parskip,topsep=5pt}
\setdescription{itemsep=0pt,partopsep=0pt,parsep=\parskip,topsep=5pt}

% 超链接样式设置
\usepackage{hyperref}
\hypersetup{
  colorlinks=true,
  linkcolor=blue,
  filecolor=blue,
  urlcolor=blue,
  citecolor=cyan,
}

\usepackage{indentfirst}

\usepackage{listings}
\usepackage[usenames,dvipsnames,svgnames, x11names]{xcolor}

\usepackage[most]{tcolorbox}
\tcbuselibrary{breakable} % 引入 breakable 库
\tcbuselibrary{skins} % 引入 skins 库

\usepackage{tikz}

% URL 正确换行
% https://liam.page/2017/05/17/help-the-url-command-from-hyperref-to-break-at-line-wrapping-point/
\makeatletter
\def\UrlAlphabet{%
  \do\a\do\b\do\c\do\d\do\e\do\f\do\g\do\h\do\i\do\j%
  \do\k\do\l\do\m\do\n\do\o\do\p\do\q\do\r\do\s\do\t%
  \do\u\do\v\do\w\do\x\do\y\do\z\do\A\do\B\do\C\do\D%
  \do\E\do\F\do\G\do\H\do\I\do\J\do\K\do\L\do\M\do\N%
  \do\O\do\P\do\Q\do\R\do\S\do\T\do\U\do\V\do\W\do\X%
  \do\Y\do\Z}
\def\UrlDigits{\do\1\do\2\do\3\do\4\do\5\do\6\do\7\do\8\do\9\do\0}
\g@addto@macro{\UrlBreaks}{\UrlOrds}
\g@addto@macro{\UrlBreaks}{\UrlAlphabet}
\g@addto@macro{\UrlBreaks}{\UrlDigits}
\makeatother

% enable subsubsubsection
% from https://tex.stackexchange.com/练习题/274212/correct-hierarchy-levels-of-pdf-bookmarks-for-custom-section-subsubsubsection
\usepackage[depth=3]{bookmark}
\setcounter{secnumdepth}{3}
\setcounter{tocdepth}{4}
\hypersetup{bookmarksdepth=4}

\makeatletter

\newcommand{\toclevel@subsubsubsection}{4}
\newcounter{subsubsubsection}[subsubsection]

\renewcommand{\thesubsubsubsection}{\thesubsubsection.\arabic{subsubsubsection}}

\newcommand{\subsubsubsection}{\@startsection{subsubsubsection}{4}{\z@}%
  {-3.25ex\@plus -1ex \@minus -.2ex}%
  {1.5ex \@plus .2ex}%
  {\normalfont\normalsize\bf\bfseries}}

\newcommand*{\l@subsubsubsection}{\@dottedtocline{4}{11em}{5em}}

\newcommand{\subsubsubsectionmark}[1]{}
\makeatother

\ExplSyntaxOn

% Setup enumerate, itemize and description
\setenumerate  { nosep }
\setitemize    { nosep }
\setdescription{ nosep }

% Setup minted
\setminted { obeytabs, tabsize=2, breaklines=true, fontsize=\footnotesize, frame=single }

% Def \filename
\NewDocumentCommand { \filename } { m }
{ \noindent  \hspace*{\fill} \\ \textit { #1 } \vspace*{ -1ex } \nopagebreak[4] }

% Def \mySamllsection
\NewDocumentCommand { \mySamllsection } { m }
{ \noindent \hspace*{\fill} \\ \textbf { #1 } \vspace*{ -1ex } \nopagebreak[4] \\ }

\NewDocumentCommand { \myGraphic } { mmm }
{
  \begin{center}
    \includegraphics[width=#1\textwidth]{#2}\\
    {#3}
  \end{center}
}

% Def \inlcpp
\NewDocumentCommand { \inlcpp }   { m }
{ \mintinline { cpp } { #1 } }

% Def cpp environment
\NewDocumentEnvironment { cpp } { }
{ \VerbatimEnvironment
  \begin { minted } [ linenos=true ] { cpp } }
{ \end   { minted } }

% Def cmake environment
\NewDocumentEnvironment { cmake } { }
{ \VerbatimEnvironment
  \begin { minted } [ linenos=true, frame=single ] { cmake } }
{ \end   { minted } }

% Def shell environment
\NewDocumentEnvironment { shell } { }
{ \VerbatimEnvironment
  \begin { minted } [ linenos=true ] { text } }
{ \end   { minted } }

\NewDocumentEnvironment { myNotic } { m }
{ %\hspace*{\fill} \\
  \begin { tcolorbox } [ breakable,colback = blue!5!white, colframe=blue!55!black ,title={#1}] }
{ \end   { tcolorbox } }

\NewDocumentEnvironment { myTip } { m }
{ %\hspace*{\fill} \\
  \begin { tcolorbox } [ breakable,colback = green!5!white, colframe=green!45!black ,title={#1}] }
{ \end   { tcolorbox } }

\NewDocumentEnvironment { myWarning } { m }
{ %\hspace*{\fill} \\
  \begin { tcolorbox } [ breakable,colback=red!5!white,colframe=red!55!black,title={#1}] }
{ \end   { tcolorbox } }

\NewDocumentCommand { \mySubsubsection } { mm }
{
\subsubsection*{\zihao{3} {#1} \hspace{0.2cm}{#2}}
\addcontentsline{toc}{subsubsection}{{#1}\hspace{0.2cm}{#2}}
}

\NewDocumentCommand { \mySubsection } { mmm }
{
\subsection*{\zihao{3}{#1}\hspace{0.2cm}{#2}}
\addcontentsline{toc}{subsection}{{#1}\hspace{0.2cm}{#2}}
\subfile{{#3}}
}

\NewDocumentCommand { \mySection } { mmm }
{
\color{black}
\pagecolor{white}
\section*{\zihao{2}{#1}\hspace{0.5cm}{#2}}
\addcontentsline{toc}{section}{{#1}\hspace{0.5cm}{#2}}
\subfile{{#3}}
}

\NewDocumentCommand { \myPart } { mmm }
{
\color{white}
\section*{\zihao{2}{#1}\hspace{0.5cm}{#2}}
\pagecolor{gray}
\addcontentsline{toc}{section}{{#1}\hspace{0.5cm}{#2}}
\subfile{{#3}}
}

% Latex如何在文本模式批量处理下划线
% https://zhuanlan.zhihu.com/p/615108006

\ExplSyntaxOff

\begin{document}
  \begin{sloppypar} %latex中一行文字出现溢出问题的解决方法
    %\maketitle

    \begin{center}
      \thispagestyle{empty}
      %\includegraphics[width=\textwidth,height=\textheight,keepaspectratio]{cover.png}
      \begin{tikzpicture}[remember picture, overlay, inner sep=0pt]
        \node at (current page.center)
        {\includegraphics[width=\paperwidth, keepaspectratio=false]{cover.png}};
      \end{tikzpicture}
      \newpage
      \thispagestyle{empty}
      \huge
      \textbf{Learn LLVM 17}
      \\[9pt]
      {\Large 学习LLVM编译器工具和核心库的初学者指南(C++版)}
      \\[9pt]
      \normalsize
      作者: Kai Nacke 和 Amy Kwan
      \\[8pt]
      \normalsize
      译者:\href{https://github.com/xiaoweiChen/Learn-LLVM-17}{陈晓伟}
      \\[8pt]
    \end{center}

    \newpage

    \pagestyle{empty}
    \tableofcontents
    \newpage

    \setsecnumdepth{section}

    \textit{
    	\mySection{}{作者致谢}{content/chapter0/0.tex}
    }
    \newpage

    \mySection{}{关于作者}{content/chapter0/1.tex}
    \newpage

    \mySection{}{关于审稿者}{content/chapter0/2.tex}
    \newpage

    \mySection{}{前言}{content/chapter0/3.tex}
    \newpage

    \myPart{第一部分}{使用LLVM构建编译器的基础知识}{content/part1/part1.tex}
    \newpage

    \mySection{第1章}{安装LLVM}{content/part1/chapter1/0.tex}
    \mySubsection{1.1.}{编译与直接安装LLVM}{content/part1/chapter1/1.tex}
    \mySubsection{1.2.}{配置环境}{content/part1/chapter1/2.tex}
    \mySubsection{1.3.}{使用代码库源码进行构建}{content/part1/chapter1/3.tex}
    \mySubsection{1.4.}{自定义构建}{content/part1/chapter1/4.tex}
    \mySubsection{1.5.}{总结}{content/part1/chapter1/5.tex}
    \newpage

    \mySection{第2章}{编译器的结构}{content/part1/chapter2/0.tex}
    \mySubsection{2.1.}{编译器的构建块}{content/part1/chapter2/1.tex}
    \mySubsection{2.2.}{算术表达式语言}{content/part1/chapter2/2.tex}
    \mySubsection{2.3.}{词法分析}{content/part1/chapter2/3.tex}
    \mySubsection{2.4.}{语法分析}{content/part1/chapter2/4.tex}
    \mySubsection{2.5.}{语义分析}{content/part1/chapter2/5.tex}
    \mySubsection{2.6.}{使用LLVM后端生成代码}{content/part1/chapter2/6.tex}
    \mySubsection{2.7.}{总结}{content/part1/chapter2/7.tex}
    \newpage

    \myPart{第二部分}{从源码到机器码}{content/part2/part2.tex}
    \newpage

    \mySection{第3章}{将源码文件转换为抽象语法树}{content/part2/chapter3/0.tex}
    \mySubsection{3.1.}{定义一种编程语言}{content/part2/chapter3/1.tex}
    \mySubsection{3.2.}{项目的目录结构}{content/part2/chapter3/2.tex}
    \mySubsection{3.3.}{管理编译器的输入文件}{content/part2/chapter3/3.tex}
    \mySubsection{3.4.}{处理用户信息}{content/part2/chapter3/4.tex}
    \mySubsection{3.5.}{构造词法分析器}{content/part2/chapter3/5.tex}
    \mySubsection{3.6.}{构建递归下降语法分析器}{content/part2/chapter3/6.tex}
    \mySubsection{3.7.}{执行语义分析}{content/part2/chapter3/7.tex}
    \mySubsection{3.8.}{总结}{content/part2/chapter3/8.tex}
    \newpage

    \mySection{第4章}{生成IR代码的基础知识}{content/part2/chapter4/0.tex}
    \mySubsection{4.1.}{AST生成IR}{content/part2/chapter4/1.tex}
    \mySubsection{4.2.}{使用AST编号生成SSA格式的IR代码}{content/part2/chapter4/2.tex}
    \mySubsection{4.3.}{设置模块和驱动程序}{content/part2/chapter4/3.tex}
    \mySubsection{4.4.}{总结}{content/part2/chapter4/4.tex}
    \newpage

    \mySection{第5章}{高级语言结构生成的IR}{content/part2/chapter5/0.tex}
    \mySubsection{5.1.}{环境要求}{content/part2/chapter5/1.tex}
    \mySubsection{5.2.}{处理数组、结构体和指针}{content/part2/chapter5/2.tex}
    \mySubsection{5.3.}{获得应用程序二进制接口}{content/part2/chapter5/3.tex}
    \mySubsection{5.4.}{为类和虚函数创建IR}{content/part2/chapter5/4.tex}
    \mySubsection{5.5.}{总结}{content/part2/chapter5/5.tex}
    \newpage

    \mySection{第6章}{生成IR代码的进阶知识}{content/part2/chapter6/0.tex}
    \mySubsection{6.1.}{抛出和捕获异常}{content/part2/chapter6/1.tex}
    \mySubsection{6.2.}{为基于类型的别名分析生成元数据}{content/part2/chapter6/2.tex}
    \mySubsection{6.3.}{生成调试元数据}{content/part2/chapter6/3.tex}
    \mySubsection{6.4.}{总结}{content/part2/chapter6/4.tex}
    \newpage

    \mySection{第7章}{优化IR}{content/part2/chapter7/0.tex}
    \mySubsection{7.1.}{环境要求}{content/part2/chapter7/1.tex}
    \mySubsection{7.2.}{LLVM通道管理器}{content/part2/chapter7/2.tex}
    \mySubsection{7.3.}{实现一个新的通道}{content/part2/chapter7/3.tex}
    \mySubsection{7.4.}{使用ppprofiler}{content/part2/chapter7/4.tex}
    \mySubsection{7.5.}{向编译器添加优化管道}{content/part2/chapter7/5.tex}
    \mySubsection{7.6.}{总结}{content/part2/chapter7/6.tex}
    \newpage


    \myPart{第三部分}{LLVM的进阶}{content/part3/part3.tex}
    \newpage

    \mySection{第8章}{TableGen语言}{content/part3/chapter8/0.tex}
    \mySubsection{8.1.}{环境要求}{content/part3/chapter8/1.tex}
    \mySubsection{8.2.}{了解TableGen语言}{content/part3/chapter8/2.tex}
    \mySubsection{8.3.}{实验TableGen语言}{content/part3/chapter8/3.tex}
    \mySubsection{8.4.}{使用TableGen文件生成C++代码}{content/part3/chapter8/4.tex}
    \mySubsection{8.5.}{TableGen的缺点}{content/part3/chapter8/5.tex}
    \mySubsection{8.6.}{总结}{content/part3/chapter8/6.tex}
    \newpage

    \mySection{第9章}{JIT编译}{content/part3/chapter9/0.tex}
    \mySubsection{9.1.}{环境要求}{content/part3/chapter9/1.tex}
    \mySubsection{9.2.}{LLVM的整体JIT的实现和用例}{content/part3/chapter9/2.tex}
    \mySubsection{9.3.}{使用JIT直接执行}{content/part3/chapter9/3.tex}
    \mySubsection{9.4.}{用LLJIT实现JIT编译器}{content/part3/chapter9/4.tex}
    \mySubsection{9.5.}{从头开始构建JIT编译器类}{content/part3/chapter9/5.tex}
    \mySubsection{9.6.}{总结}{content/part3/chapter9/6.tex}
    \newpage

    \mySection{第10章}{使用LLVM工具进行调试}{content/part3/chapter10/0.tex}
    \mySubsection{10.1.}{环境要求}{content/part3/chapter10/1.tex}
    \mySubsection{10.2.}{用消毒器检测应用程序}{content/part3/chapter10/2.tex}
    \mySubsection{10.3.}{使用libFuzzer查找bug}{content/part3/chapter10/3.tex}
    \mySubsection{10.4.}{使用XRay进行性能分析}{content/part3/chapter10/4.tex}
    \mySubsection{10.5.}{使用clang静态分析器检查源代码}{content/part3/chapter10/5.tex}
    \mySubsection{10.6.}{创建基于clang的工具}{content/part3/chapter10/6.tex}
    \mySubsection{10.7.}{总结}{content/part3/chapter10/7.tex}
    \newpage

    \myPart{第四部分}{创建自定义后端}{content/part4/part4.tex}
    \newpage

    \mySection{第11章}{目标描述}{content/part4/chapter11/0.tex}
    \mySubsection{11.1.}{为新后端做准备}{content/part4/chapter11/1.tex}
    \mySubsection{11.2.}{将新架构添加到Triple类中}{content/part4/chapter11/2.tex}
    \mySubsection{11.3.}{扩展LLVM中的ELF文件格式定义}{content/part4/chapter11/3.tex}
    \mySubsection{11.4.}{创建目标描述}{content/part4/chapter11/4.tex}
    \mySubsection{11.5.}{为LLVM添加M88k后端}{content/part4/chapter11/5.tex}
    \mySubsection{11.6.}{实现汇编解析器}{content/part4/chapter11/6.tex}
    \mySubsection{11.7.}{创建反汇编器}{content/part4/chapter11/7.tex}
    \mySubsection{11.8.}{总结}{content/part4/chapter11/8.tex}
    \newpage

    \mySection{第12章}{指令选择}{content/part4/chapter12/0.tex}
    \mySubsection{12.1.}{定义调用约定规则}{content/part4/chapter12/1.tex}
    \mySubsection{12.2.}{通过DAG进行指令选择}{content/part4/chapter12/2.tex}
    \mySubsection{12.3.}{添加寄存器和指令信息}{content/part4/chapter12/3.tex}
    \mySubsection{12.4.}{向下转译空帧}{content/part4/chapter12/4.tex}
    \mySubsection{12.5.}{发出机器指令}{content/part4/chapter12/5.tex}
    \mySubsection{12.6.}{创建目标机器和子目标}{content/part4/chapter12/6.tex}
    \mySubsection{12.7.}{全局指令的选择}{content/part4/chapter12/7.tex}
    \mySubsection{12.8.}{进化后端}{content/part4/chapter12/8.tex}
    \mySubsection{12.9.}{总结}{content/part4/chapter12/9.tex}
    \newpage

    \mySection{第13章}{超越指令选择}{content/part4/chapter13/0.tex}
    \mySubsection{13.1.}{为LLVM添加新机器功能通道}{content/part4/chapter13/1.tex}
    \mySubsection{13.2.}{将新目标集成到clang前端}{content/part4/chapter13/2.tex}
    \mySubsection{13.3.}{针对不同的CPU架构}{content/part4/chapter13/3.tex}
    \mySubsection{13.4.}{总结}{content/part4/chapter13/4.tex}
    \newpage

  \end{sloppypar}
\end{document}



================================================
FILE: README.md
================================================
# Learn LLVM 17
A beginner's guide to learning LLVM compiler tools and core libraries with C++ 

(*使用C++学习LLVM编译器和核心库的初学者教程*)

 <a href=""><img src="cover.png" height="256px" align="right"></a>

* 作者:Kai Nacke 和 Amy Kwan

* 译者:陈晓伟

* 原文发布时间:2024年1月

> [!IMPORTANT]
> 翻译是译者用自己的思想,换一种语言,对原作者想法的重新阐释。鉴于我的学识所限,误解和错译在所难免。如果你能买到本书的原版,且有能力阅读英文,请直接去读原文。因为与之相较,我的译文可能根本不值得一读。
>
> <p align="right"> — 云风,程序员修炼之道第2版译者</p>

## 本书概述

构造编译器是一项复杂而迷人的任务。LLVM项目为编译器提供了可重用的组件,LLVM核心库实现了世界级的优化代码生成器,可以为所有主流CPU架构翻译与源语言无关的机器码中间表示,许多编程语言的编译器已经在使用LLVM。

本书将介绍如何实现自己的编译器,以及如何使用LLVM来实现。您将了解编译器的前端如何将源代码转换为抽象语法树,以及如何从中生成中间表示(IR)。此外,还将探索在编译器中添加一个优化管道,可将IR编译为高性能的机器码。

LLVM框架可以通过多种方式进行扩展,读者将了解如何向LLVM添加通道,甚至是一个全新的后端。高级主题,如编译不同的CPU架构和扩展clang和clang静态分析器与自己的插件和检查器也包括在内。本书遵循一种实用的方法,并附有示例源代码,读者可以在自己的项目中应用相应的代码。



## 作者简介

**Kai Nacke**是一名专业IT架构师,目前居住在加拿大多伦多。毕业于德国多特蒙德技术大学的计算机科学专业。他关于通用哈希函数的毕业论文,被评为最佳论文。

他在IT行业工作超过20年,在业务和企业应用程序的开发和架构方面有丰富的经验。他在研发一个基于LLVM/Clang的编译器。

几年来,他一直是LDC(基于LLVM的D语言编译器)的维护者。在Packt出版过《D Web Development》一书,他也曾在自由和开源软件开发者欧洲会议(FOSDEM)的LLVM开发者室做过演讲。



## 本书相关

* github翻译地址:https://github.com/xiaoweiChen/Learn-LLVM-17

* 译文的LaTeX 环境配置:https://www.cnblogs.com/1625--H/p/11524968.html

  * 禁用拼写检查:https://blog.csdn.net/weixin_39278265/article/details/87931348

  * 使用xelatex编译时需要添加`-shell-escape`和`-8bit`选项,例如:

    `xelatex -synctex=1 -interaction=nonstopmode -shell-escape -8bit "C++-Standard-Library".tex`

  * 为了内容中表格和目录索引能正常生成,需要至少两次连续编译

* vscode中配置LaTeX:https://blog.csdn.net/Ruins_LEE/article/details/123555016



================================================
FILE: content/chapter0/0.tex
================================================
\begin{center}

写一本书需要很多时间和精力。没有我的妻子坦尼娅和女儿波琳娜的支持和理解,这本书是不可能完成的。谢谢你们一直鼓励我!

由于一些个人的挑战,这个项目处于危险之中,很感谢艾米作为作者加入这个项目。若没有她,这本书就不会像现在这样好。

再次感谢Packt的团队,不仅对我的写作提供指导,而且对我缓慢的写作表现出理解,并一直激励我坚持下去。我欠他们一个大大的感谢。

\hspace*{\fill} \\

- Kai Nacke

\hspace*{\fill} \\

2023年对我来说是非常具有变革意义的一年,我将自己的LLVM知识贡献给这本书,是今年如此重要的原因之一。我从来没有想过Kai会找到我,开始这段激动人心的旅程,与大家分享LLVM 17 !感谢Kai,感谢他的技术指导和指导,感谢Packt的团队,当然还有我的家人和亲人,感谢他们在我写这本书的过程中给予的支持和动力。

\hspace*{\fill} \\

- Amy Kwan

\end{center}

================================================
FILE: content/chapter0/1.tex
================================================
\textbf{Kai Nacke}是一名专业的IT架构师,目前居住在加拿大多伦多,拥有德国多特蒙德技术大学(Technical University of Dortmund)计算机科学文凭。他关于通用散列函数的毕业论文是当时最好的论文之一。

Kai在IT行业拥有超过20年的经验,在业务和企业应用程序的开发和架构方面拥有丰富的专业知识。在他目前的职位上,开发了一个基于LLVM/clang的编译器。

几年来,Kai一直担任LDC(基于llvm的D编译器)的维护者。他是《D Web Development》和《Learn LLVM 12》的作者,这两本书都由Packt出版。过去,他还是自由和开源软件开发者欧洲会议(FOSDEM)的LLVM开发者工作室的讲师。

\hspace*{\fill} \\

\textbf{Amy Kwan}是一名编译器开发人员,目前居住在加拿大多伦多。Amy来自加拿大大草原,拥有萨斯喀彻温大学计算机科学学士学位。她在职位上,利用LLVM技术作为后端编译器开发人员。此前,Amy曾与Kai Nacke一起在2022年的LLVM开发者大会上发表演讲。






















================================================
FILE: content/chapter0/2.tex
================================================
\textbf{Akash Kothari}是伊利诺伊LLVM编译器研究实验室的研究助理。他在伊利诺伊大学厄巴纳-香槟分校获得计算机科学博士学位。Akash专注于性能工程、程序合成、形式语义和验证,他还对探索计算和编程系统的历史非常感兴趣。

\hspace*{\fill} \\

\textbf{Shuo Niu}拥有计算机工程硕士学位,是编译技术领域的一股活跃力量。在英特尔PSG专注于FPGA HLD编译器的五年里,领导了编译器中端优化器的创新。他在开发尖端功能方面的专业知识,使用户能够在FPGA板上实现显着的性能提升。

================================================
FILE: content/chapter0/3.tex
================================================
构造编译器是一项复杂而迷人的任务。LLVM项目为编译器提供了可重用的组件,LLVM核心库实现了世界级的优化代码生成器,可以为所有主流CPU架构翻译与源语言无关的机器码中间表示,许多编程语言的编译器已经在使用LLVM。

本书将介绍如何实现自己的编译器,以及如何使用LLVM来实现。您将了解编译器的前端如何将源代码转换为抽象语法树,以及如何从中生成中间表示(IR)。此外,还将探索在编译器中添加一个优化管道,可将IR编译为高性能的机器码。

LLVM框架可以通过多种方式进行扩展,读者将了解如何向LLVM添加通道,甚至是一个全新的后端。高级主题,如编译不同的CPU架构和扩展clang和clang静态分析器与自己的插件和检查器也包括在内。本书遵循一种实用的方法,并附有示例源代码,读者可以在自己的项目中应用相应的代码。

\mySubsubsection{}{新版本增加的内容}

有一个新的章节,专门介绍在LLVM中使用的TableGen语言的概念和语法,读者可以利用它来定义类,记录和整个LLVM后端。此外,本书还介绍了后端开发的重点,其中讨论了可以为LLVM后端实现的各种新后端概念,例如:实现GlobalISel指令框架和开发机器功能通道。

\mySubsubsection{}{适读人群}

本书是为有兴趣学习LLVM框架的编译器开发人员、爱好者和工程师编写的。对于希望使用基于编译器的工具进行代码分析和改进的C++软件工程师,以及希望获得更多LLVM基本知识的LLVM库的工程师。要理解本书所涵盖的概念,必须具有C++中级编程经验。

\mySubsubsection{}{本书内容}

\textit{第1章,安装LLVM},解释了如何设置和使用你的开发环境,了解如何自行编译LLVM库,以及自定义构建过程。

\textit{第2章,编译器的结构},对编译器组件的概述,最后可以实现第一个生成LLVM IR的编译器。

\textit{第3章,将源码文件转换为抽象语法树},详细介绍了如何实现编译器的前端。为一种小型编程语言创建前端编译器,最后构建抽象语法树。

\textit{第4章,生成IR代码的基础知识},展示了如何从抽象语法树生成LLVM IR。将实现语言的编译器,生成汇编文本或目标代码文件。

\textit{第5章,高级语言结构生成的IR},说明了如何将高级编程语言中常见的源语言特性转换为LLVM IR。将了解聚合数据类型的转换、实现类继承和虚函数的各种选项,以及如何遵循系统的应用程序二进制接口。

\textit{第6章,生成IR代码的进阶知识},展示了如何在源语言中为异常处理语句生成LLVM IR。还将学习如何为基于类型的别名分析添加元数据,以及如何向生成的LLVM IR添加调试信息,并扩展编译器生成的元数据。

\textit{第7章,优化IR},解释了LLVM通道管理器。将实现自己的通道,既作为LLVM的一部分,也作为插件,将了解如何将通道添加到优化通道流水线中。

\textit{第8章,TableGen语言},介绍了LLVM自己的领域特定语言TableGen。该语言用于减少开发人员的编码工作,将了解在TableGen语言中定义数据的不同方法,以及如何在后端使用。

\textit{第9章,JIT编译},讨论了如何使用LLVM实现一个即时(JIT)编译器。将以两种不同的方式为LLVM IR实现自己的JIT编译器。

\textit{第10章,使用LLVM工具进行调试},探讨了LLVM的各种库和组件的细节,可以识别应用程序中的错误,使用消毒器来识别缓冲区溢出和其他错误;使用libFuzzer库,使用随机数据作为输入来测试函数;XRay将帮助程序找到性能瓶颈。可使用clang静态分析器在源代码级别识别错误,并且将了解到可以向分析器添加自己的检查器,还将了解如何使用自己的插件对clang进行扩展。

\textit{第11章,目标描述},解释了如何添加对新的CPU架构的支持。本章讨论了必要的和可选的步骤,如定义寄存器和指令,开发指令选择,以及支持汇编和反汇编程序。

\textit{第12章,指令选择},演示了两种不同的指令选择方法,特别解释了SelectionDAG和GlobalISel是如何工作的,并展示了如何在目标中实现这些功能,基于前一章的例子。此外,将了解如何对指令选择进行调试和测试。

\textit{第13章,超越指令选择},解释了如何通过探索超越指令选择的概念来完成后端实现。这包括添加新的机器通道来实现特定于目标的任务,这些主题对于简单后端来说是不必要的,但对于高度优化的后端来说是有趣的,例如:交叉编译到另一个CPU架构。

\mySubsubsection{}{编译环境}

您需要一台运行Linux、Windows、Mac OS X或FreeBSD的计算机,并为操作系统安装了开发工具链。所需工具请参见表格,所有工具都需要配置到shell的搜索路径中。

% Please add the following required packages to your document preamble:
% \usepackage{longtable}
% Note: It may be necessary to compile the document several times to get a multi-page table to line up properly
\begin{longtable}{|l|l|}
\hline
\textbf{书中涉及的软件/硬件} & \textbf{操作系统} \\ \hline
\endfirsthead
%
\endhead
%
\begin{tabular}[c]{@{}l@{}}C/C++编译器:\\ gcc 7.1.0或更高版本, clang 3.0或更高版本,\\ Apple clang 10.0 或更高版本,\\ Visual Studio 2019 16.7或更高版本\end{tabular} &
\begin{tabular}[c]{@{}l@{}}Linux(any), Windows,\\ Mac OS X或FreeBSD\end{tabular} \\ \hline
CMake 3.20.0或更高版本                          & \\ \hline
Ninja 1.11.1                                   & \\ \hline
Python 3.6或更高版本                            & \\ \hline
Git 2.39或更高版本                   & \\ \hline
\end{longtable}


要创建第10章“使用LLVM工具调试”中的火焰图,需要从\url{https://github.com/brendangregg/FlameGraph}获取安装脚本。要运行安装脚本,还需要安装最新版本的Perl。要查看图形,还需要能够显示SVG文件的Web浏览器,这是所有现代浏览器都能做到的。要在同一章中查看Chrome Trace Viewer可视化,需要安装Chrome浏览器。

\textbf{如果正在使用本书的数字版本,我们建议您自己输入代码或通过GitHub存储库访问代码(下一节提供链接),将避免复制和粘贴代码。}

\mySubsubsection{}{下载示例}

可以从GitHub网站\url{https://github.com/PacktPublishing/Learn-LLVM-17}下载本书的示例代码。如果有对代码的更新,也会在现有的GitHub存储库中更新。

我们还在\url{https://github.com/PacktPublishing/}上提供了丰富的图书和视频目录中的其他代码包。可以一起拿来看看!


\mySubsubsection{}{联系方式}

我们欢迎读者的反馈。

\textbf{反馈}:如果你对这本书的任何方面有疑问,需要在你的信息的主题中提到书名,并给我们发邮件到\url{customercare@packtpub.com}。

\textbf{勘误}:尽管我们谨慎地确保内容的准确性,但错误还是会发生。如果您在本书中发现了错误,请向我们报告,我们将不胜感激。请访问\url{www.packtpub.com/support/errata},选择相应书籍,点击勘误表提交表单链接,并输入详细信息。

\textbf{盗版}:如果您在互联网上发现任何形式的非法拷贝,非常感谢您提供地址或网站名称。请通过\url{copyright@packt.com}与我们联系,并提供材料链接。

\textbf{如果对成为书籍作者感兴趣}:如果你对某主题有专长,又想写一本书或为之撰稿,请访问\url{authors.packtpub.com}。


\mySubsubsection{}{欢迎评论}

我们很想听听读者们对本书的看法!欢迎请点击这里直接进入这本书的亚马逊评论页面,分享你的反馈。

您的评论对我们和技术社区都很重要,将帮助确保书籍内容的品质。



================================================
FILE: content/part1/chapter1/0.tex
================================================
为了学习如何使用LLVM,最好先从源代码开始编译LLVM。LLVM是一个伞形项目,GitHub库包含属于LLVM的所有项目的源代码。每个LLVM项目都位于存储库的顶级目录中。除了克隆库之外,本地环境还必须安装构建系统所需的所有工具。

本章中,将学习以下主题:

\begin{itemize}
\item
准备环境,将展示如何设置构建系统

\item
克隆库并使用源码构建,了解如何获得LLVM源代码,以及如何编译和安装LLVM核心库和clang与CMake和Ninja

\item
自定义构建过程,将讨论影响构建过程的各种可能性
\end{itemize}










================================================
FILE: content/part1/chapter1/1.tex
================================================
可以使用各种源来安装LLVM二进制文件。若使用的是Linux,则其发行版包含LLVM库。为什么要自己编译LLVM呢?

首先,并非所有安装包都包含使用LLVM进行开发所需的所有文件,自己编译和安装LLVM可以避免这个问题。另一个原因是,LLVM可定制,通过构建LLVM,将了解如何自定义LLVM,这将使读者能够诊断将LLVM应用程序放到另一个平台运行时可能出现的问题。最后,本书的第三部分,将会对LLVM进行扩展,所以需要有能自行构建LLVM的能力。

但在开始使用时,完全可以避免编译LLVM。若想要走这条路,只需要安装下一节中描述的相关工具即可。

\begin{myNotic}{Note}
许多Linux发行版将LLVM分成几个包。请确保安装了开发包。以Ubuntu为例,需要安装llvm-dev包。请确定安装了LLVM 17。对于其他版本,本书中的示例可能需要修改。
\end{myNotic}

================================================
FILE: content/part1/chapter1/2.tex
================================================

要使用LLVM,开发系统应该运行一个通用的操作系统,如Linux、FreeBSD、macOS或Windows。可以在不同的模式下构建LLVM和clang,启用调试符号的构建最多需要30 GB的空间。所需的磁盘空间在很大程度上取决于所选择的构建选项。例如,在发布模式下仅构建LLVM核心库,只针对一个平台,需要最少2 GB的可用磁盘空间。

为了减少编译时间,快速的CPU(例如:时钟速度为2.5 GHz的四核CPU)和快速的SSD也很有帮助。甚至可以在小型设备(如Raspberry Pi)上构建LLVM——需要花费很多时间。本书中的示例是在一台笔记本电脑上开发的,该笔记本电脑采用Intel四核CPU,时钟速度为2.7 GHz,具有40GB RAM和2.5TB SSD磁盘空间。

您的开发系统必须安装一些必备软件,来回顾一下这些软件包的最低要求版本。

要从GitHub查看源代码,需要Git(\url{https://git-scm.com/})。GitHub帮助页面建议至少使用1.17.10版本。由于过去发现的已知安全问题,建议使用最新的可用版本,在撰写本文时为2.39.1。

LLVM项目使用CMake(\url{https://cmake.org/})作为构建文件生成器,至少为3.20.0。CMake可以为各种构建系统生成构建文件。本书中,使用了Ninja(\url{https://ninja-build.org/}),因为它速度快,并且可以在所有平台上使用,建议使用最新版本1.11.1。

显然,还需要一个C/C++编译器。LLVM项目是基于C++17标准,用现代C++编写的。需要一个兼容的编译器和标准库。以下编译器可以与LLVM 17一起工作(已测试):

\begin{itemize}
\item
gcc 7.1.0或更高版本

\item
clang 5.0或更高版本

\item
Apple clang 10.0或更高版本

\item
Visual Studio 2019 16.7或更高版本
\end{itemize}

\begin{myTip}{Tip}
随着LLVM项目的进一步发展,编译器的需求很可能会发生变化。一般来说,应该使用系统可用的最新编译器版本。
\end{myTip}

Python(\url{https://python.org/})用于生成构建文件和运行测试套件,至少为3.8。

虽然本书没有涉及,但可能需要使用Make,而非Ninja,所以需要使用GNU Make(\url{https://www.gnu.org/software/make/})3.79或更高版本。这两种构建工具的用法非常相似。对于下面描述的场景,将每个命令中的ninja替换为make就可以了。

LLVM还依赖于zlib库(\url{https://www.zlib.net/}),至少为1.2.3.4版本。与往常一样,建议使用最新版本1.2.13。

要安装必备软件,最简单的方法是从操作系统中使用包管理器。下面几节中,将为主流操作系统显示安装软件所需的命令。

\mySubsubsection{1.2.1.}{Ubuntu}

Ubuntu 22.04使用apt包管理器。大多数基本的工具都已经安装好了,只缺少开发工具。要一次安装所有软件包,可以输入以下命令:

\begin{shell}
$ sudo apt -y install gcc g++ git cmake ninja-build zlib1g-dev
\end{shell}

\mySubsubsection{1.2.2.}{Fedora和RedHat}

Fedora 37和RedHat Enterprise Linux 9的包管理器名为dnf。和Ubuntu一样,大多数基本的工具都已经安装好了。要一次安装所有软件包,可以输入以下命令:

\begin{shell}
$ sudo dnf –y install gcc gcc-c++ git cmake ninja-build zlib-devel
\end{shell}

\mySubsubsection{1.2.3.}{FreeBSD}

在FreeBSD 13或更高版本上,必须使用pkg包管理器。FreeBSD与基于linux的系统的不同之处在于已经安装了clang编译器。要一次安装所有其他软件包,可以输入以下命令:

\begin{shell}
$ sudo pkg install –y git cmake ninja zlib-ng
\end{shell}

\mySubsubsection{1.2.4.}{OS X}

在OS X上开发,最好从Apple商店安装Xcode。虽然本书中没有使用Xcode IDE,但它附带了所需的C/C++编译器和相关工具。对于其他工具的安装,可以使用包管理器Homebrew(\url{https://brew.sh/})。要一次安装所有软件包,可以输入以下命令:

\begin{shell}
$ brew install git cmake ninja zlib
\end{shell}

\mySubsubsection{1.2.5.}{Windows}

和OS X一样,Windows没有包管理器。对于C/C++编译器,需要下载个人免费使用的Visual Studio Community 2022(\url{https://visualstudio.microsoft.com/vs/community/})。请确保安装了名为Desktop Development with C++的工作负载。可以使用包管理器Scoop(\url{https://scoop.sh/})来安装其他包。按照网站上的描述安装Scoop之后,从Windows菜单中打开VS 2022的x64 Native Tools Command Prompt。要安装所需的软件包,输入以下命令:

\begin{shell}
$ scoop install git cmake ninja python gzip bzip2 coreutils
$ scoop bucket add extras
$ scoop install zlib
\end{shell}

请密切关注Scoop的输出。对于Python和zlib包,建议添加一些注册表项。其他软件需要这些条目才能找到这些软件包。要添加注册表项,最好复制并粘贴来自Scoop的输出,如下所示:

\begin{shell}
$ %HOMEPATH%\scoop\apps\python\current\install-pep-514.reg
$ %HOMEPATH%\scoop\apps\zlib\current\register.reg
\end{shell}

每个命令之后,注册表编辑器将弹出一个消息窗口,询问是否真的要导入这些注册表项,需要单击Yes以完成导入。现在,已经安装了所有所需的工具。

对于本书中的所有示例,必须在VS 2022中使用x64本机工具命令提示符。使用此命令提示符,编译器将自动添加到搜索路径中。

\begin{myTip}{Tip}
LLVM代码库非常大。为了方便地导航源代码,建议使用一个IDE,可以跳转到类的定义,并搜索源代码。我们发现Visual Studio Code(\url{https://code.visualstudio.com/download})是一个可扩展的跨平台IDE,使用起来非常舒服,但这不是运行本书示例的必要条件。
\end{myTip}


================================================
FILE: content/part1/chapter1/3.tex
================================================

准备好构建工具后,可以从GitHub中下载LLVM项目,并构建LLVM。这一过程在所有平台上都是相同的:

\begin{enumerate}
\item
配置Git

\item
克隆库

\item
创建构建目录

\item
生成构建系统文件

\item
最后,编译和安装LLVM
\end{enumerate}

先从配置Git开始吧!

\mySubsubsection{1.3.1.}{配置Git}

LLVM项目使用Git进行版本控制。若以前没有使用过Git,在继续之前应该先做一些Git的基本配置:设置用户名和电子邮件地址。提交修改,将使用这两个信息。

可以使用以下命令检查是否已经在Git中配置了电子邮件和用户名:

\begin{shell}
$ git config user.email
$ git config user.name
\end{shell}

前面的命令将输出在使用Git时已经设置的电子邮件和用户名,但若是第一次设置用户名和电子邮件,可以输入以下命令进行第一次配置。以下命令中,可以简单地将Jane替换为您的姓名,将jane@email.org替换为您的电子邮件:

\begin{shell}
$ git config --global user.email "jane@email.org"
$ git config --global user.name "Jane"
\end{shell}

这些命令更改全局Git配置。在Git存储库中,可以不指定-{}-global选项在本地覆盖这些值。

默认情况下,Git使用vi编辑器提交消息。若喜欢其他编辑器,可以以类似的方式更改配置。例如:要使用nano编辑器,可以输入以下命令:

\begin{shell}
$ git config --global core.editor nano
\end{shell}

有关Git的更多信息,请参阅Git版本控制手册(\url{https:// www.packtpub.com/product/git-version-control-cookbook-secondedition/9781789137545})。

现在可以从GitHub克隆LLVM了。

\mySubsubsection{1.3.2.}{克隆库}

克隆库的命令在所有平台上基本上是相同的。仅在Windows上,建议关闭行结束符的自动翻译。

在所有非windows平台上,输入以下命令克隆库:

\begin{shell}
$ git clone https://github.com/llvm/llvm-project.git
\end{shell}

仅在Windows上,添加禁用行结束符自动翻译的选项,可输入以下内容:

\begin{shell}
$ git clone --config core.autocrlf=false \
  https://github.com/llvm/llvm-project.git
\end{shell}

这个Git命令将最新的源代码从GitHub克隆到一个名为llvm-project的本地目录中。现在使用以下命令,将当前目录切换到llvm-project目录:

\begin{shell}
$ cd llvm-project
\end{shell}

目录中是所有LLVM项目,每个项目都在自己的目录中。最值得注意的是,LLVM核心库位于LLVM子目录中。LLVM项目使用分支用于后续版本开发(“release/17.x”)和标签(“llvmg -17.0.1”)来标记某个版本。使用前面的clone命令,可以获得当前的开发状态。本书使用LLVM 17。要将LLVM 17的第一个版本检出到一个名为llvm-17的分支中,可以输入以下命令:

\begin{shell}
$ git checkout -b llvm-17 llvmorg-17.0.1
\end{shell}

通过前面的步骤,就克隆了整个库,并从标记创建了分支。这是最灵活的方法。

Git还允许只克隆一个分支或标记(包括历史记录)。使用git clone -{}-branch release/X https://github.com/llvm/llvm-project,只克隆版本release/17.x分支及其历史。然后,就可以看到LLVM 17发布分支的最新状态。若需要确切的发布版本,只需要像之前一样从发布标签创建一个分支。

使用-{}-depth=1选项,就是Git的浅克隆,可以避免下载太多历史记录。这节省了时间和空间,但显然限制了在本地可以做的事情,包括根据发布标记签出分支。

\mySubsubsection{1.3.3.}{创建构建目录}

LLVM不支持内联构建,并且需要一个单独的构建目录。最简单的方法是在llvm-project目录中创建,这是当前目录。简单起见,将构建目录命名为build。这里,Unix和Windows系统的命令不同。在类Unix系统上,可以使用以下命令:

\begin{shell}
$ mkdir build
\end{shell}

在Windows上,使用以下命令:

\begin{shell}
$ md build
\end{shell}

现在,就已经准备好使用该目录下的CMake工具创建构建系统文件了。

\mySubsubsection{1.3.4.}{生成构建系统文件}

为了使用Ninja生成编译LLVM和clang的构建系统文件,可以运行以下命令:

\begin{shell}
$ cmake -G Ninja -DCMAKE_BUILD_TYPE=Release \
  -DLLVM_ENABLE_PROJECTS=clang -B build -S llvm
\end{shell}

-G选项告诉CMake为哪个系统生成构建文件。该选项的常用值如下:

\begin{itemize}
\item
Ninja – for the Ninja build system

\item
Unix Makefiles – for GNU Make

\item
Visual Studio 17 VS2022 – for Visual Studio and MS Build

\item
Xcode – for Xcode projects
\end{itemize}

使用-B选项,告诉CMake构建目录的路径。类似地,可以使用-S选项指定源目录。可以通过使用-D选项设置各种变量来影响生成过程,通常以CMAKE\_(由CMAKE定义)或LLVM\_(由LLVM定义)为前缀。

如前所述,我们也对与LLVM一起编译clang感兴趣。使用LLVM\_ENABLE\_PROJECTS=clang,允许CMake除了为LLVM生成构建文件外,还为clang生成构建文件。此外,CMAKE\_BUILD\_TYPE=Release变量告诉CMAKE它应该为“发布”构建生成构建文件。

-G选项的默认值取决于平台,而构建类型的默认值取决于工具链,但可以使用环境变量定义自己的首选项。CMAKE\_GENERATOR变量控制生成器,CMAKE\_BUILD\_TYPE变量指定构建类型。若使用bash或类似的shell,可以这样设置变量:

\begin{shell}
$ export CMAKE_GENERATOR=Ninja
$ export CMAKE_BUILD_TYPE=Release
\end{shell}

若使用的是Windows命令提示符,可以这样设置变量:

\begin{shell}
$ set CMAKE_GENERATOR=Ninja
$ set CMAKE_BUILD_TYPE=Release
\end{shell}

有了这些设置,创建构建系统文件的命令就变成了下面这样,这样更容易输入:

\begin{shell}
$ cmake -DLLVM_ENABLE_PROJECTS=clang -B build -S llvm
\end{shell}

将在自定义构建过程一节中找到更多关于CMake变量的信息。

\mySubsubsection{1.3.5.}{编译和安装LLVM}

生成构建文件后,可以使用以下命令编译LLVM和clang:

\begin{shell}
$ cmake --build build
\end{shell}

我们告诉CMake在配置步骤中生成Ninja文件,所以这个命令在底层运行Ninja。但若为支持多构建配置的生成器(如Visual Studio)生成构建文件,则需要使用-{}-config选项指定要用于构建的配置。根据硬件资源的不同,该命令的运行时间从15分钟(具有大量CPU内核、内存和快速存储的服务器)到几个小时(具有有限内存的双核Windows笔记本)不等。

默认情况下,Ninja使用所有可用的CPU内核。这有利于提高编译速度,但可能会阻止其他任务的运行;例如,在Windows操作系统的笔记本电脑上,几乎不可能在运行Ninja时上网。幸运的是,可以使用-j选项限制资源使用。

假设有四个可用的CPU内核,而Ninja应该只使用两个(因为你有并行任务要运行)。就使用以下命令进行编译:

\begin{shell}
$ cmake --build build -j2
\end{shell}

编译完成后,运行测试检查是否一切都如预期的那样工作:

\begin{shell}
$ cmake --build build --target check-all
\end{shell}

同样,该命令的运行时随着可用硬件资源的不同而变化很大。checkall Ninja目标运行所有测试用例。为每个包含测试用例的目录生成目标。使用check-llvm而不是check-all会运行LLVM测试,但不会运行clang测试,checkllvm-codegen仅从LLVM(即llvm/test/CodeGen目录)运行CodeGen目录中的测试。

也可以进行快速手动检查。LLVM应用程序之一是LLVM编译器llc。若使用-version选项运行,会显示LLVM的版本,主机CPU和所有支持的架构:

\begin{shell}
$ build/bin/llc --version
\end{shell}

若在编译LLVM时遇到困难,应该查阅入门LLVM系统文档(\url{https://releases.llvm.org/17.0.1/docs/GettingStarted.html#common-problems})中的常见问题部分,了解常见问题的解决方案。

最后一步,安装二进制文件:

\begin{shell}
$ cmake --install build
\end{shell}

类Unix系统上,安装目录是/usr/local。在Windows上,使用C:\verb|\|Program Files\verb|\|LLVM。这可以修改,下一节将解释如何实现。


================================================
FILE: content/part1/chapter1/4.tex
================================================

CMake系统使用CMakeLists.txt文件中的项目描述。顶层文件位于llvm目录“llvm/CMakeLists.txt”。其他目录也有CMakeLists.txt文件,这些文件在生成过程中递归包含。

根据项目描述中提供的信息,CMake检查安装了哪些编译器,检测库和符号,并创建构建系统文件,例如build.ninja或Makefile(取决于所选择的生成器)。也可以定义可重用的模块,例如:检测LLVM是否已安装的函数。这些脚本放在特殊的cmake目录(llvm/cmake)中,在生成过程中会自动搜索该目录。

构建过程可以通过定义CMake变量来定制。命令行选项-D用于将变量设置为一个值,这些变量在CMake脚本中使用。CMake自己定义的变量几乎总是以CMAKE\_为前缀,这些变量可以在所有项目中使用。由LLVM定义的变量以LLVM\_为前缀,但只能在项目定义中包含LLVM时使用。

\mySubsubsection{1.4.1.}{可定义的CMake变量}

有些变量是用环境变量的值初始化的。最值得注意的是CC和CXX,定义了用于构建的C和C++编译器。CMake尝试使用当前shell搜索路径自动定位C和C++编译器,会选择找到的第一个编译器。若本地安装了多个编译器,例如gcc和clang或不同版本的clang,那么这可能不是您构建LLVM所需的编译器。

假设使用clang17作为C编译器,clang++17作为C++编译器。可以在Unix shell中调用CMake,方法如下:

\begin{shell}
$ CC=clang17 CXX=clang++17 cmake –B build –S llvm
\end{shell}

这只会为cmake的调用设置环境变量的值,可以为编译器可执行文件指定一个绝对路径。

CC是CMAKE\_C\_COMPILER CMAKE变量的默认值,CXX是CMAKE\_CXX\_COMPILER CMAKE变量的默认值。可以直接设置CMake变量,而不是使用环境变量:

\begin{shell}
$ cmake –DCMAKE_C_COMPILER=clang17 \
  -DCMAKE_CXX_COMPILER=clang++17 –B build –S llvm
\end{shell}

CMake定义的其他相关变量如下所示:

% Please add the following required packages to your document preamble:
% \usepackage{longtable}
% Note: It may be necessary to compile the document several times to get a multi-page table to line up properly
\begin{longtable}{|l|l|}
\hline
\textbf{变量名} &
\textbf{功能} \\ \hline
\endfirsthead
%
\endhead
%
CMAKE\_INSTALL\_PREFIX &
\begin{tabular}[c]{@{}l@{}}
这是安装过程中每个安装路径的前缀。Unix上默认为/usr/local和\\ Windows上默认为C:\textbackslash Program Files\textbackslash{}\textless{}Project\textgreater。要在/opt/llvm目录\\ 下安装LLVM,需要设置-DCMAKE\_INSTALL\_PREFIX=/opt/llvm。\\ 二进制文件将复制到/opt/llvm/bin文件夹下,库文件将复制到\\ /opt/llvm/lib文件夹下,以此类推。
\end{tabular} \\ \hline
CMAKE\_BUILD\_TYPE &
\begin{tabular}[c]{@{}l@{}}
不同类型的构建需要不同的设置。例如,调试构建需要指定生成调\\ 试符号的选项,通常链接到系统库的调试版本。相比之下,发布版\\本使用针对库的生产版本的优化标志和链接。此变量仅用于只能处\\ 理一种构建类型的构建系统,例如:Ninja或Make。对于IDE构建系\\ 统,将生成所有配置,并且需要使用IDE在构建类型之间进行切换。\\ 取值范围如下所示: \\
\\
DEBUG:构建带有调试符号 \\
RELEASE:构建优化运行速度 \\
RELWITHDEBINFO:构建发布版本,但带有调试符号 \\
MINSIZEREL:构建会生成尺寸最小的二进制文件(也是一种优化) \\
\\
默认构建类型由CMAKE\_BUILD\_TYPE变量设置。若未设置此变\\量,则默认值取决于所使用的工具链,并且通常为空。为了生成\\ 发布版本的构建,可以设置-DCMAKE\_BUILD\_TYPE=RELEASE。
\end{tabular} \\ \hline
\begin{tabular}[c]{@{}l@{}}CMAKE\_C\_FLAGS \\ CMAKE\_CXX\_FLAGS\end{tabular} &
\begin{tabular}[c]{@{}l@{}}
这些是编译C和C++源文件时使用的编译选项,初始值取自\\ CFLAGS和CXXFLAGS环境变量。
\end{tabular} \\ \hline
CMAKE\_MODULE\_PATH &
\begin{tabular}[c]{@{}l@{}}
指定CMake模块搜索的其他目录。在默认目录之前搜索指定的目\\ 录,以分号分隔的目录列表。
\end{tabular} \\ \hline
PYTHON\_EXECUTABLE &
\begin{tabular}[c]{@{}l@{}}
若没有找到Python解释器,或者在安装了多个版本的情况下选择了\\ 错误的解释器,则可以将此变量设置为Python二进制文件的路径。\\ 这个变量只有在包含CMake的Python模块时才有效(LLVM就是这\\ 种情况)。
\end{tabular} \\ \hline
\end{longtable}

\begin{center}
表1.1 - CMake定义的其他相关变量
\end{center}

CMake为变量提供了内置帮助。{}-help-variable var选项打印var变量的帮助信息。可以输入以下命令来获取CMAKE\_BUILD\_TYPE的信息:

\begin{shell}
$ cmake --help-variable CMAKE_BUILD_TYPE
\end{shell}

也可以用下面的命令列出所有的变量:

\begin{shell}
$ cmake --help-variable-list
\end{shell}

这个清单很长,可能将输出管道传输到相应的处理程序中会更合适。

\mySubsubsection{1.4.2.}{使用LLVM定义的构建配置变量}

除了没有内置帮助之外,LLVM定义的构建配置变量的工作方式与CMake定义的相同。最有用的变量如下表所示,对首次安装LLVM的用户有用的变量和对更高级的LLVM用户有用的变量。

% Please add the following required packages to your document preamble:
% \usepackage{longtable}
% Note: It may be necessary to compile the document several times to get a multi-page table to line up properly
\begin{longtable}{|l|l|}
\hline
\textbf{变量名} &
\textbf{功能} \\ \hline
\endfirsthead
%
\endhead
%
LLVM\_TARGETS\_TO\_BUILD &
\begin{tabular}[c]{@{}l@{}}
LLVM支持不同CPU架构的代码生成。默认\\ 情况下,所有这些目标都已构建。使用此变\\ 量指定要构建的目标列表,以分号分隔。目\\ 前支持的目标有AArch64、AMDGPU、\\ ARM、AVR、BPF、Hexagon、Lanai、\\ LoongArch、Mips、MSP430、NVPTX、\\ PowerPC、RISCV、Sparc、SystemZ、VE、\\ WebAssembly、X86和XCore。\\
\\
all可以作为所有目标的简写。名称区分大小\\ 写。要只启用PowerPC和SystemZ目标,可\\ 以指定-DLLVM\_TARGETS\_TO\_BUILD=\\ "PowerPC;SystemZ"。
\end{tabular} \\ \hline
LLVM\_EXPERIMENTAL\_TARGETS\_TO\_BUILD &
\begin{tabular}[c]{@{}l@{}}
除了官方支持的架构外,LLVM源代码树还\\ 包含实验目标。这些目标还在开发中,通常\\ 还不支持后端的全部功能。目前的实验得架\\ 构有ARC、CSKY、DirectX、M68k、SPIRV\\ 和Xtensa。要构建M68k目标,可以设置\\ -DLLVM\_EXPERIMENTAL\_TARGETS\_TO\\ \_BUILD=M68k。
\end{tabular} \\ \hline
LLVM\_ENABLE\_PROJECTS &
\begin{tabular}[c]{@{}l@{}}
这是要构建的项目列表,以分号分隔。项目\\ 的源代码必须与llvm目录在同一级(并排布\\ 局)。当前的列表是bolt、clang、\\ clang-tools-extra、compiler-rt、\\ cross-project-tests、libc、libclc、lld、lldb、\\ mlir、openmp、polly和pstl。\\
\\all可以用作此列表中所有项目的简写,可以\\ 在这里指定flang项目。由于一些特殊的构建\\ 需求,它还不是all列表的一部分。\\
\\
要将clang和bolt与LLVM一起构建,可以\\ 设置-DLLVM\_ENABLE\_PROJECT=\\ "clang;bolt"。
\end{tabular} \\ \hline
\end{longtable}

\begin{center}
表1.2 - 对于首次安装LLVM的用户有用的变量
\end{center}

% Please add the following required packages to your document preamble:
% \usepackage[normalem]{ulem}
% \useunder{\uline}{\ul}{}
% \usepackage{longtable}
% Note: It may be necessary to compile the document several times to get a multi-page table to line up properly
\begin{longtable}{|l|l|}
\hline
\textbf{变量名} &
\textbf{功能} \\ \hline
\endfirsthead
%
\endhead
%
LLVM\_ENABLE\_ASSERTIONS &
\begin{tabular}[c]{@{}l@{}}
若设置为ON,则启用断言检查。这些检查有助于发现错\\ 误,在开发过程中非常有用。对于DEBUG版本,默认值\\ 为ON,否则为OFF。要打开断言检查(例如,对于\\ RELEASE版本),可以设置\\ -DLLVM\_ENABLE\_ASSERTIONS=ON。
\end{tabular} \\ \hline
LLVM\_ENABLE\_EXPENSIVE\_CHECKS &
\begin{tabular}[c]{@{}l@{}}
这将启用一些大开销的检查,这些检查可能会减慢编译速\\ 度或消耗大量内存,默认值为OFF。要打开这些检查,可\\ 以指定-DLLVM\_ENABLE\_EXPENSIVE\_CHECKS=ON。
\end{tabular} \\ \hline
LLVM\_APPEND\_VC\_REV &
\begin{tabular}[c]{@{}l@{}}
若使用-version命令行选项,llc等LLVM工具除了显示其\\ 他信息外,还会显示它们所基于的LLVM版本。该版本\\ 信息基于C代码中的LLVM\_REVISION宏。默认情况下,\\ 不仅LLVM版本,而且当前Git哈希值也是版本信息的一\\ 部分。若正在跟踪主分支的开发,就能清楚地表明该工具\\ 基于哪个Git提交。若不需要,则可以使用\\ -DLLVM\_APPEND\_VC\_REV=OFF关闭此功能。
\end{tabular} \\ \hline
LLVM\_ENABLE\_THREADS &
\begin{tabular}[c]{@{}l@{}}
若检测到线程库(通常是pthread库),LLVM会自动包含线\\ 程支持。在这种情况下,LLVM会假定编译器支持TLS\\ (线程本地存储)。若不想要线程支持或者本地编译器不支\\ 持TLS,那么可以设置-DLLVM\_ENABLE\_THREADS=OFF\\ 关闭该功能。
\end{tabular} \\ \hline
LLVM\_ENABLE\_EH &
\begin{tabular}[c]{@{}l@{}}
LLVM项目不使用C++异常处理,因此在默认情况下关闭\\ 异常支持。此设置可能与项目所链接的其他库不兼容。若\\ 需要,可以指定-DLLVM\_ENABLE\_EH=ON来启用异常支\\ 持。
\end{tabular} \\ \hline
LLVM\_ENABLE\_RTTI &
\begin{tabular}[c]{@{}l@{}}
LLVM使用一个轻量级的自构建系统来获取运行时类型信\\ 息,C++ RTTI的生成在默认情况下是关闭的。与异常处理\\ 支持一样,这可能与其他库不兼容。要开启C++ RTTI的生\\ 成,可以设置-DLLVM\_ENABLE\_RTTI=ON。
\end{tabular} \\ \hline
LLVM\_ENABLE\_WARNINGS &
\begin{tabular}[c]{@{}l@{}}
可能的话,编译LLVM应该不会生成任何警告消息,所以\\ 打印警告消息的选项在默认情况下是打开的。要关闭它,\\ 可以设置-DLLVM\_ENABLE\_WARNINGS=OFF。
\end{tabular} \\ \hline
LLVM\_ENABLE\_PEDANTIC &
\begin{tabular}[c]{@{}l@{}}
LLVM源代码应符合C/C++语言标准,默认情况下启用对\\ 源代码进行严格检查。若可能的话,还会禁用特定于编译\\ 器的扩展。要关闭此设置,可以设置\\ -DLLVM\_ENABLE\_PEDANTIC=OFF。
\end{tabular} \\ \hline
LLVM\_ENABLE\_WERROR &
\begin{tabular}[c]{@{}l@{}}
若设置为ON,那么所有警告都被视为错误——发现警告,\\ 编译就会中止。这有助于在源代码中找到所有剩余的警告。\\ 默认情况下,它是关闭的。要打开它,可以设置\\ -DLLVM\_ENABLE\_WERROR=ON。
\end{tabular} \\ \hline
LLVM\_OPTIMIZED\_TABLEGEN &
\begin{tabular}[c]{@{}l@{}}
通常,tablegen工具与LLVM的其他部分使用相同的选项\\ 构建。同时,tablegen是用来生成大部分代码的生成器。\\ 因此,tablegen在调试构建中要慢得多,显著增加了编译\\ 时间。若将此选项设置为ON,则即使在调试构建中,\\ tablegen也会在编译时打开优化,这可能会减少编译时间。\\ 默认为OFF。要打开它,可以指定\\ -DLLVM\_OPTIMIZED\_TABLEGEN=ON。
\end{tabular} \\ \hline
LLVM\_USE\_SPLIT\_DWARF &
\begin{tabular}[c]{@{}l@{}}
若构建编译器是gcc或clang,那么打开该选项将指示编译\\ 器在单独的文件中生成DWARF调试信息。目标文件大小\\ 的减小,减少了调试构建的链接时间,默认为OFF。要打\\ 开它,可以设置-DLLVM\_USE\_SPLIT\_DWARF=ON。
\end{tabular} \\ \hline
\end{longtable}

\begin{center}
表1.3 - LLVM进阶用户的变量
\end{center}

\begin{myNotic}{Note}
LLVM定义了更多的CMake变量,可以在关于CMake的LLVM文档\url{https://releases.llvm.org/17.0.1/docs/CMake.html#llvm-specific-variables}中找到完整的列表。前面的列表只包含最可能需要的部分。
\end{myNotic}


















================================================
FILE: content/part1/chapter1/5.tex
================================================
本章中,准备了开发机器来编译LLVM,克隆了GitHub库,并编译了LLVM和clang版本。构建过程可以用CMake变量自定义,了解了有用的变量以及如何更改它们。有了这些知识,就可以根据自己的需要调整LLVM了。

下一节中,我们将进一步了解编译器的结构,将探索编译器内部的不同组件,以及其中发生的不同类型的分析——特别是词法、语法和语义分析。最后,还会简要介绍使用LLVM后端进行代码生成的接口。

================================================
FILE: content/part1/chapter2/0.tex
================================================
编译器技术是计算机科学中一个研究领域,其任务是将源语言翻译成机器码。通常,此任务分为三个部分:前端、中端和后端。前端主要处理源语言,而中端执行转换并改进代码,后端负责生成机器码。由于LLVM核心库提供了中端和后端,我们将在本章中专注于前端的介绍。

本章中,将学习以下主题:

\begin{itemize}
\item
编译器的构建块,将了解编译器中的常用组件

\item
算术表达式语言,将介绍一种示例语言,并展示如何使用语法来定义语言

\item
词法分析,讨论如何实现语言的词法分析器

\item
语法分析,包括从语法构造解析器

\item
语义分析,将了解如何实现语义检查

\item
使用LLVM后端生成代码,讨论如何与LLVM后端进行接口,并将前面的所有阶段粘合在一起,创建完整的编译器
\end{itemize}














================================================
FILE: content/part1/chapter2/1.tex
================================================
自从有了计算机,编程语言就开发了数千种。事实证明,所有编译器都必须解决相同的任务,并且编译器的实现最好根据这些任务进行结构化。总得来说,有三个组成部分。前端将源代码转换为中间表示(IR);中端在IR上执行转换,其目标是提高性能或减少代码的大小;后端将IR生成为机器码。LLVM核心库为所有主流平台提供了一个由复杂转换和后端组成的中端。

此外,LLVM核心库还定义了一个中间表示,用作中间端和后端的输入。这种设计的优点是,只需要关心编程语言的前端即可。

前端的输入是源代码,通常是一个文本文件。为了使它更有意义,前端首先标识语言的单词,例如数字和标识符,通常称为标记。这一步由词法分析器执行。其次,分析了符号构成的句法结构。所谓的解析器执行这个步骤,结果是生成了抽象语法树(AST)。

最后,前端需要由语义分析器检查,代码是否遵守编程语言的规则。若没有检测到错误,则将AST转换为IR,并移交给中端处理。

下面几节中,将为表达式语言构造一个编译器,该编译器将根据其输入生成LLVM IR。LLVM llc静态编译器表示后端,将IR编译成目标代码,这一切都从定义语言开始。切记,本章中所有文件的C++实现,都将包含在src/目录中。

================================================
FILE: content/part1/chapter2/2.tex
================================================

算术表达式是每一种编程语言的一部分。下面是一个算术表达式计算语言calc的例子,calc表达式会编译成一个计算以下表达式的应用程序:

\begin{shell}
with a, b: a * (4 + b)
\end{shell}

表达式中使用的变量必须用关键字with声明。该程序会编译成一个应用程序,该程序向用户询问a和b变量的值,并输出结果。

示例总是简单并容易理解的,但作为编译器作者,需要比这更全面的实现和测试规范。首先,编程语言的载体是语法。

\mySubsubsection{2.2.1.}{描述程序设计语言语法的形式}

语言中的元素,例如关键字、标识符、字符串、数字和操作符,称为标记(token)。从这个意义上说,程序是一个标记序列,语法指定了哪些序列是有效的。

通常,语法以扩展的Backus-Naur形式(EBNF)编写。语法规则有左边和右边,左边只有一个符号,叫做非终结符;右侧由非终结符、记号和用于替代和重复的元符号组成。来看看calc语言的语法:

\begin{shell}
calc : ("with" ident ("," ident)* ":")? expr ;
expr : term (( "+" | "-" ) term)* ;
term : factor (( "*" | "/") factor)* ;
factor : ident | number | "(" expr ")" ;
ident : ([a-zAZ])+ ;
number : ([0-9])+ ;
\end{shell}

第一行中,calc是非终结符。若没有特别说明,则语法的第一个非终结符是开始符号。冒号(:)是规则左右两边之间的分隔符,"with"、","和":"是表示这个字符串的标记,括号用于分组。组可以是可选的,也可以是重复的。右括号后的问号(?)表示一个可选的组。星号*表示零次或多次重复,加号+表示一次或多次重复。ident和expr是非终结符,处理它们使用的是另一种规则。分号(;)表示规则的结束,第二行中的管道|表示另一种选择。最后,最后两行的括号[]表示字符类。有效字符写在括号内。例如,字符类[a-zA-Z]匹配一个大写或小写字母,而([a-zA-Z])+匹配这些字母中的一个或多个。看上去,这就是一个正则表达式。

\mySubsubsection{2.2.2.}{语法如何帮助编译器作者?}

这样的语法看起来像一个玩具,但对编译器作者很有意义。首先,定义所有标记,这是创建词法分析器所需的,语法规则可以转化为解析器。若有解析器是否正确工作的问题,则语法就可以作为标准进行衡量。

然而,语法并不能定义编程语言的所有方面。语法的含义——语义——也必须定义。为此目的的形式也开发出来了,因为是在最初引入该语言时拟定的,所以通常在纯文本中指定。

了解了这些知识,接下来的两节将介绍词法分析如何将输入转换为标记序列,以及语法如何使用C++进行语法分析。

================================================
FILE: content/part1/chapter2/3.tex
================================================
正如在前一节的示例,编程语言由许多元素组成,如关键字、标识符、数字、操作符等。词法分析器的任务是获取文本输入并从中创建一个标记序列。calc语言由,:,+,-,*,/,(,)和正则表达式([a- za -z])+(标识符)和([0-9])+(数字)组成。需要为每个标记分配唯一编号,以使标记更容易处理。

\mySubsubsection{2.3.1.}{手写词法分析器}

词法分析器的实现通常称为Lexer。这里,创建一个名为Lexer.h的头文件,并开始定义Token:

\begin{cpp}
#ifndef LEXER_H
#define LEXER_H

#include "llvm/ADT/StringRef.h"
#include "llvm/Support/MemoryBuffer.h"
\end{cpp}

llvm::MemoryBuffer类提供对内存块的只读访问,内存块由文件的内容填充,在缓冲区的末尾添加一个尾零字符('\verb|\|x00')。我们使用这个特性来读取缓冲区,而不必在每次访问时检查缓冲区的长度。StringRef类封装了一个指向C字符串及其长度的指针。因为对长度进行了保存,所以字符串不需要像普通的C字符串那样以零字符('\verb|\|x00')结束。StringRef实例允许指向由MemoryBuffer管理的内存。

了解了这一点后,我们开始实现Lexer类:

\begin{enumerate}
\item
首先,Token类包含前面提到的唯一标记编号的枚举定义:

\begin{cpp}
class Lexer;

class Token {
    friend class Lexer;

public:
    enum TokenKind : unsigned short {
        eoi, unknown, ident, number, comma, colon, plus,
        minus, star, slash, l_paren, r_paren, KW_with
    };
\end{cpp}

除了为每个标记定义成员外,还添加了两个值:eoi和unknown。eoi表示输入结束,当处理完输入的所有字符时返回。unknown在词法级别发生错误时使用,例如,\#不是该语言的标记,将映射为unknown。

\item
除了枚举之外,该类还有一个Text成员,该成员指向标记文本的开头,使用了前面提到的StringRef类:

\begin{cpp}
private:
    TokenKind Kind;
    llvm::StringRef Text;

public:
    TokenKind getKind() const { return Kind; }
    llvm::StringRef getText() const { return Text; }
\end{cpp}

这对于语义处理很有意义,例如:对于标识符,了解名称很有用。

\item
is()和isOneOf()方法用于测试标识是否属于某种类型。isOneOf()方法使用可变的模板,允许可变数量的参数:

\begin{cpp}
    bool is(TokenKind K) const { return Kind == K; }
    bool isOneOf(TokenKind K1, TokenKind K2) const {
        return is(K1) || is(K2);
    }
    template <typename... Ts>
    bool isOneOf(TokenKind K1, TokenKind K2, Ts... Ks) const {
        return is(K1) || isOneOf(K2, Ks...);
    }
};
\end{cpp}

\item
头文件中,Lexer类本身也有类似的接口:

\begin{cpp}
class Lexer {
    const char *BufferStart;
    const char *BufferPtr;

public:
    Lexer(const llvm::StringRef &Buffer) {
        BufferStart = Buffer.begin();
        BufferPtr = BufferStart;
    }

    void next(Token &token);

private:
    void formToken(Token &Result, const char *TokEnd,
                   Token::TokenKind Kind);
};
#endif
\end{cpp}

除了构造函数之外,公共接口只有next()方法,该方法返回下一个标识。该方法的作用类似于迭代器,总是前进到下一个可用标识。该类的唯一成员是指向输入开头和下一个未处理字符的指针,假设缓冲区以0结束(就像C字符串一样)。

\item
在Lexer.cpp文件中实现Lexer类,从一些帮助函数开始分类字符:

\begin{cpp}
#include "Lexer.h"
namespace charinfo {
    LLVM_READNONE inline bool isWhitespace(char c) {
        return c == ' ' || c == '\t' || c == '\f' ||
        c == '\v' ||
        c == '\r' || c == '\n';
    }
    LLVM_READNONE inline bool isDigit(char c) {
        return c >= '0' && c <= '9';
    }
    LLVM_READNONE inline bool isLetter(char c) {
        return (c >= 'a' && c <= 'z') ||
        (c >= 'A' && c <= 'Z');
}
}
\end{cpp}

这些函数使条件更具有可读性。

\begin{myNotic}{Note}
没有使用<cctype>标准库头文件提供的函数有两个原因。首先,这些函数根据环境中定义的语言环境改变行为。例如,区域设置是德语区域,那么德语的变音符可以分类为字母。这在编译器中通常是不需要的。其次,由于函数的形参类型为int,因此需要从char类型进行转换。这种转换的结果取决于char是视为有符号类型还是无符号类型,这会导致移植性问题。
\end{myNotic}

\item
前一节对语法的了解中,知道了该语言的所有符号,但语法没有定义应该忽略的字符。例如,空格或换行字符只添加空白,通常应该忽略。next()一开始就会忽略这些字符:

\begin{cpp}
void Lexer::next(Token &token) {
    while (*BufferPtr &&
    charinfo::isWhitespace(*BufferPtr)) {
        ++BufferPtr;
    }
\end{cpp}

\item
接下来,确保仍然有字符需要处理:

\begin{cpp}
    if (!*BufferPtr) {
        token.Kind = Token::eoi;
        return;
    }
\end{cpp}

至少有一个字符需要处理。

\item
首先检查字符是小写还是大写。所以,标记要么是标识符,要么是with关键字,因为标识符的正则表达式也匹配关键字。最常见的解决方案是,收集正则表达式匹配的字符,并检查字符串是否恰好是关键字:

\begin{cpp}
    if (charinfo::isLetter(*BufferPtr)) {
        const char *end = BufferPtr + 1;
        while (charinfo::isLetter(*end))
            ++end;
        llvm::StringRef Name(BufferPtr, end - BufferPtr);
        Token::TokenKind kind =
            Name == "with" ? Token::KW_with : Token::ident;
        formToken(token, end, kind);
        return;
    }
\end{cpp}

formToken()私有方法用于填充标记。

\item
接下来,检查数字。与前面的代码片段非常相似:

\begin{cpp}
    else if (charinfo::isDigit(*BufferPtr)) {
        const char *end = BufferPtr + 1;
        while (charinfo::isDigit(*end))
            ++end;
        formToken(token, end, Token::number);
        return;
    }
\end{cpp}

现在只剩下由固定字符串定义的标记。

\item
这很容易用switch完成。由于所有这些标记都只有一个字符,因此使用CASE预处理器宏来减少编码量:

\begin{cpp}
else {
    switch (*BufferPtr) {
        #define CASE(ch, tok) \
        case ch: formToken(token, BufferPtr + 1, tok); break
        CASE('+', Token::plus);
        CASE('-', Token::minus);
        CASE('*', Token::star);
        CASE('/', Token::slash);
        CASE('(', Token::Token::l_paren);
        CASE(')', Token::Token::r_paren);
        CASE(':', Token::Token::colon);
        CASE(',', Token::Token::comma);
        #undef CASE
\end{cpp}

\item
最后,需要检查不支持的字符:

\begin{cpp}
        default:
            formToken(token, BufferPtr + 1, Token::unknown);
        }
        return;
    }
}
\end{cpp}

只有formToken()私有辅助方法仍然缺失。

\item
填充Token实例的成员,并更新指向下一个未处理字符的指针:

\begin{cpp}
void Lexer::formToken(Token &Tok, const char *TokEnd,
                      Token::TokenKind Kind) {
    Tok.Kind = Kind;
    Tok.Text = llvm::StringRef(BufferPtr,
                               TokEnd - BufferPtr);
    BufferPtr = TokEnd;
}
\end{cpp}

\end{enumerate}

下一节中,我们将了解如何构造用于语法分析的解析器。








================================================
FILE: content/part1/chapter2/4.tex
================================================
语法分析由解析器完成,接下来我们将实现解析器。它的基础是前面小节中的语法和词法分析器。解析过程的结果是一个动态数据结构,称为抽象语法树(AST)。AST是输入的一个非常紧凑的表示,非常适合语义分析。

首先,将实现解析器,再了解AST的解析过程。

\mySubsubsection{2.4.1.}{手写解析器}

解析器的接口在头文件Parser.h中定义,以一些include声明开始:

\begin{cpp}
#ifndef PARSER_H
#define PARSER_H

#include "AST.h"
#include "Lexer.h"
#include "llvm/Support/raw_ostream.h"
\end{cpp}

AST.h头文件声明AST的接口,将在后面展示。LLVM的编码指南禁止使用<iostream>库,所以包含了等效LLVM功能的头文件。这里需要发出一个错误消息:

\begin{enumerate}
\item
Parser类首先声明一些私有成员:

\begin{cpp}
class Parser {
    Lexer &Lex;
    Token Tok;
    bool HasError;
\end{cpp}

Lex和Tok是前一节中类的实例。Tok存储下一个标记(预检),Lex用于从输入中检索下一个标记。HasError标志表示是否检测到错误。

\item
有几个方法处理这个标记:

\begin{cpp}
    void error() {
        llvm::errs() << "Unexpected: " << Tok.getText()
        << "\n";
        HasError = true;
    }

    void advance() { Lex.next(Tok); }

    bool expect(Token::TokenKind Kind) {
        if (Tok.getKind() != Kind) {
            error();
            return true;
        }
        return false;
    }

    bool consume(Token::TokenKind Kind) {
        if (expect(Kind))
            return true;
        advance();
        return false;
    }
\end{cpp}

advance()从词法分析器检索下一个标记。Expect()测试预检是否具有预期的类型,否则发出错误消息。最后,若预检具有预期的类型,则consume()将检索下一个标记。若发出错误消息,则将HasError标志设置为true。

\item
对于语法的每个非终结符,声明一个解析规则的方法:

\begin{cpp}
    AST *parseCalc();
    Expr *parseExpr();
    Expr *parseTerm();
    Expr *parseFactor();
\end{cpp}

\begin{myNotic}{Note}
没有标识和编号的方法。这些规则只返回标记,并用相应的标记替换。
\end{myNotic}

\item
接下来是公共接口。构造函数初始化所有成员,并从词法分析器获取第一个标记:

\begin{cpp}
public:
    Parser(Lexer &Lex) : Lex(Lex), HasError(false) {
        advance();
    }
\end{cpp}

\item
需要一个函数来获取错误标志的值:

\begin{cpp}
    bool hasError() { return HasError; }
\end{cpp}

\item
最后,parse()方法是解析的主要入口:

\begin{cpp}
    AST *parse();
};

#endif
\end{cpp}

\end{enumerate}

\mySubsubsection{2.4.1.1}{解析器的实现}

让我们深入了解解析器的实现!

\begin{enumerate}
\item
Parser.cpp文件的实现中,以parse()开始:

\begin{cpp}
#include "Parser.h"

AST *Parser::parse() {
    AST *Res = parseCalc();
    expect(Token::eoi);
    return Res;
}
\end{cpp}

parse()的要点是使用了整个输入。还记得第一节中的解析示例中,添加了一个特殊符号来表示输入的结束吗?需要在这里进行检查。

\item
parseCalc()实现相应的规则。因为其他解析方法也遵循相同的模式,所以有必要仔细研究一下这个方法。回顾一下第一节的规则:

\begin{cpp}
calc : ("with" ident ("," ident)* ":")? expr ;
\end{cpp}

\item
该方法首先声明一些局部变量:

\begin{cpp}
AST *Parser::parseCalc() {
    Expr *E;
    llvm::SmallVector<llvm::StringRef, 8> Vars;
\end{cpp}

\item
第一个决定是是否必须解析可选组。组以with标记开始,将标记与以下值进行比较:

\begin{cpp}
if (Tok.is(Token::KW_with)) {
    advance();
\end{cpp}

\item
接下来,需要一个标识符:

\begin{cpp}
    if (expect(Token::ident))
        goto _error;
    Vars.push_back(Tok.getText());
    advance();
\end{cpp}

若标识符存在,则将其保存在Vars vector中。否则,就是一个语法错误,单独处理。

\item
接下来是一个重复组,解析了更多的标识符,并用逗号分隔:

\begin{cpp}
    while (Tok.is(Token::comma)) {
        advance();
        if (expect(Token::ident))
            goto _error;
        Vars.push_back(Tok.getText());
        advance();
    }
\end{cpp}

重复组以标记(,)开头,标记的测试成为while循环的条件,实现零或更多次的重复。循环内的标识符和以前一样进行处理。

\item
最后,可选组需要在末尾加一个冒号:

\begin{cpp}
    if (consume(Token::colon))
        goto _error;
}
\end{cpp}

\item
最后,必须解析expr规则:

\begin{cpp}
    E = parseExpr();
\end{cpp}

\item
有了这个调用,规则的解析就完成了。收集到的信息可以用于为该规则创建AST节点:

\begin{cpp}
    if (Vars.empty()) return E;
    else return new WithDecl(Vars, E);
\end{cpp}
\end{enumerate}

现在只缺少错误处理。检测语法错误很容易,但从中恢复却异常复杂。这里使用了一种简单的方法,称为恐慌模式。

恐慌模式下,标记会从标记流中删除,直到找到一个解析器可以使用它继续工作。大多数编程语言都有表示结束的符号,例如:C++中,a;(语句结束)或\}(语句块结束)。这样的标记是很好的候选对象。

另一方面,错误可能是正在寻找的符号丢失了,所以可能在解析器继续之前删除了许多标记。这并不像听起来那么糟糕,现在更重要的是编译器的速度。若出现错误,开发人员查看第一条错误消息,修复它,然后重新启动编译器。这与使用打孔卡完全不同,打孔卡非常重要的是获取尽可能多的错误消息,因为编译器的下一次运行可能要在第二天才可以进行。

\mySubsubsection{2.4.1.2}{错误处理}

这里没有使用一些标记来查找,而是使用了另一组标记。对于每个非终结符,在规则中都有一组标记可以跟随该非终结符:

\begin{enumerate}
\item
在calc中,只有输入的结尾跟在这个非终结符后面。实现很简单:

\begin{cpp}
_error:
    while (!Tok.is(Token::eoi))
        advance();
    return nullptr;
}
\end{cpp}

\item
其他解析方法的构造也类似。parseExpr()是对expr规则的翻译:

\begin{cpp}
Expr *Parser ::parseExpr() {
    Expr *Left = parseTerm() ;
    while (Tok.isOneOf(Token::plus, Token::minus)) {
        BinaryOp::Operator Op =
            Tok.is(Token::plus) ? BinaryOp::Plus :
                                  BinaryOp::Minus;
        advance();
        Expr *Right = parseTerm();
        Left = new BinaryOp(Op, Left, Right);
    }
    return Left;
}
\end{cpp}

规则中的重复组可转换为while循环,使用isOneOf()方法简化了对多个标记的检查。

\item
术语规则的编码看起来都一样:

\begin{cpp}
Expr *Parser::parseTerm() {
    Expr *Left = parseFactor();
    while (Tok.isOneOf(Token::star, Token::slash)) {
        BinaryOp::Operator Op =
            Tok.is(Token::star) ? BinaryOp::Mul :
                                  BinaryOp::Div;
        advance();
        Expr *Right = parseFactor();
        Left = new BinaryOp(Op, Left, Right);
    }
    return Left;
}
\end{cpp}

此方法与parseExpr()非常相似,有的读者可能想将它们合并为一个方法。在语法中,可以使用一个规则处理乘法和加性操作符。使用两个规则的优点是,操作符的优先级与求值的数学顺序非常吻合。若结合了这两个规则,需要在其他地方弄清楚求值顺序。

\item
最后,需要实现解析factor规则:

\begin{cpp}
Expr *Parser::parseFactor() {
    Expr *Res = nullptr;
    switch (Tok.getKind()) {
        case Token::number:
        Res = new Factor(Factor::Number, Tok.getText());
        advance(); break;
\end{cpp}

这里不使用if和else if语句链,而使用switch语句,因为每个选项都只以一个标记开头。一般来说,应该考虑喜欢使用哪种翻译模式。若以后需要更改解析方法,并不是每个方法都有不同的实现语法规则的方式,这就很方便修改了。

\item
若使用switch语句,错误处理将在默认情况下发生:

\begin{cpp}
    case Token::ident:
        Res = new Factor(Factor::Ident, Tok.getText());
        advance(); break;
    case Token::l_paren:
        advance();
        Res = parseExpr();
        if (!consume(Token::r_paren)) break;
    default:
        if (!Res) error();
\end{cpp}

由于失败,需要在这里避免发出错误消息。

\item
若括号表达式中存在语法错误,则已经发出错误消息。这个守卫可以防止第二个错误消息:

\begin{cpp}
        while (!Tok.isOneOf(Token::r_paren, Token::star,
                            Token::plus, Token::minus,
                            Token::slash, Token::eoi))
            advance();
    }
    return Res;
}
\end{cpp}

\end{enumerate}

这很简单,不是吗?记住使用的模式之后,根据语法规则编写解析器是一项乏味的工作。这种类型的解析器称为递归下降解析器。

\begin{myNotic}{并非所有语法都能用于构造递归下降解析器}
语法必须满足某些条件才能适合于构造递归下降解析器,这类语法称为LL(1)。事实上,能在网上能找到的大多数语法都不属于这类语法。大多数关于编译器结构理论的书籍都解释了这一点的原因。关于这个话题的经典书籍是所谓的“龙书”——《编译器:原则,技术和工具》,由Aho, Lam, Sethi和Ullman编写。
\end{myNotic}

\mySubsubsection{2.4.2.}{抽象语法树}

解析过程的结果是AST,AST是输入程序的另一种紧凑表示,捕获了基本信息。许多编程语言都有作为分隔符的符号,但这些符号没有进一步的含义。例如:在C++中,分号;表示单个语句的结束,这些信息对解析器很重要。当将语句转换为内存中的表示形式后,分号就不重要,可以删除了。

看一下示例表达式语言的第一条规则,with关键字、逗号(,)和冒号(:)对于程序的含义并不重要。重要的是声明的变量列表,可以在表达式中使用。结果是只需要几个类来记录信息:Factor保存一个数字或标识符,BinaryOp保存算术运算符和表达式的左右两边,WithDecl存储声明的变量和表达式的列表。AST和Expr仅用于创建公共类层次结构。

除了来自解析输入的信息外,还支持使用访问者模式对树进行遍历。这都在AST.h头文件中有所体现:

\begin{enumerate}
\item
开始于访问者接口:

\begin{cpp}
#ifndef AST_H
#define AST_H

#include "llvm/ADT/SmallVector.h"
#include "llvm/ADT/StringRef.h"

class AST;
class Expr;
class Factor;
class BinaryOp;
class WithDecl;

class ASTVisitor {
    public:
    virtual void visit(AST &){};
    virtual void visit(Expr &){};
    virtual void visit(Factor &) = 0;
    virtual void visit(BinaryOp &) = 0;
    virtual void visit(WithDecl &) = 0;
};
\end{cpp}

访问者模式需要知道要访问的每个类。每个类也引用访问者,所以在文件的顶部声明所有类。请注意,AST和Expr的visit()方法有一个默认实现,但不做任何事情。

\item
AST类是层次结构的根:

\begin{cpp}
class AST {
public:
    virtual ~AST() {}
    virtual void accept(ASTVisitor &V) = 0;
};
\end{cpp}

\item
类似地,Expr是与表达式相关的AST类的根:

\begin{cpp}
class Expr : public AST {
public:
    Expr() {}
};
\end{cpp}

\item
Factor类存储一个数字或一个变量的名称:

\begin{cpp}
class Factor : public Expr {
public:
    enum ValueKind { Ident, Number };

private:
    ValueKind Kind;
    llvm::StringRef Val;

public:
    Factor(ValueKind Kind, llvm::StringRef Val)
        : Kind(Kind), Val(Val) {}
    ValueKind getKind() { return Kind; }
    llvm::StringRef getVal() { return Val; }
    virtual void accept(ASTVisitor &V) override {
        V.visit(*this);
    }
};
\end{cpp}

本例中,数字和变量的处理方式几乎相同,所以可以只创建一个AST节点类来表示。Kind成员实例代表两种情况中的哪一种。在更复杂的语言中,通常希望使用不同的AST类,例如:用于数字的NumberLiteral类和用于变量引用的VariableAccess类。

\item
BinaryOp类保存表达式求值所需的数据:

\begin{cpp}
class BinaryOp : public Expr {
public:
    enum Operator { Plus, Minus, Mul, Div };

private:
    Expr *Left;
    Expr *Right;
    Operator Op;

public:
    BinaryOp(Operator Op, Expr *L, Expr *R)
        : Op(Op), Left(L), Right(R) {}
    Expr *getLeft() { return Left; }
    Expr *getRight() { return Right; }
    Operator getOperator() { return Op; }
    virtual void accept(ASTVisitor &V) override {
        V.visit(*this);
    }
};
\end{cpp}

与解析器相反,BinaryOp类不区分乘法运算符和加法运算符。操作符的优先级在树结构中隐式可用。

\item
最后,WithDecl类存储了声明的变量和表达式:

\begin{cpp}
class WithDecl : public AST {
    using VarVector =
        llvm::SmallVector<llvm::StringRef, 8>;
    VarVector Vars;
    Expr *E;

public:
    WithDecl(llvm::SmallVector<llvm::StringRef, 8> Vars,
            Expr *E)
        : Vars(Vars), E(E) {}
    VarVector::const_iterator begin()
                                { return Vars.begin(); }
    VarVector::const_iterator end() { return Vars.end(); }
    Expr *getExpr() { return E; }
    virtual void accept(ASTVisitor &V) override {
        V.visit(*this);
    }
};
#endif
\end{cpp}

\end{enumerate}

AST是在解析过程中构造的。语义分析检查树是否符合语言的含义(例如,使用已声明的变量),并可能对树进行扩充,树将用于代码生成。

================================================
FILE: content/part1/chapter2/5.tex
================================================
语义分析器遍历AST并检查语言的各种语义规则,例如:变量必须在使用前声明,或者变量的类型必须在表达式中兼容。若语义分析器发现可以改进的情况,还可以输出警告。对于示例表达式语言,语义分析器必须检查是否声明了每个使用的变量(这是该语言所要求的)。一个可能的扩展(这里没有实现)是,在声明的变量未使用时输出警告。

语义分析器在Sema类中实现,由semantic()方法执行。下面是完整的Sema.h头文件内容:

\begin{cpp}
#ifndef SEMA_H
#define SEMA_H

#include "AST.h"
#include "Lexer.h"

class Sema {
    public:
    bool semantic(AST *Tree);
};

#endif
\end{cpp}

实现在Sema.cpp文件中。有趣的部分是语义分析,使用访问者实现。其基本思想是,每个声明变量的名称存储在一个集合中。创建集合时,可以检查每个名字的唯一性,再检查给定的名字是否在集合中:

\begin{cpp}
#include "Sema.h"
#include "llvm/ADT/StringSet.h"

namespace {
class DeclCheck : public ASTVisitor {
    llvm::StringSet<> Scope;
    bool HasError;
    enum ErrorType { Twice, Not };
    void error(ErrorType ET, llvm::StringRef V) {
        llvm::errs() << "Variable " << V << " "
                     << (ET == Twice ? "already" : "not")
                     << " declared\n";
        HasError = true;
    }
public:
    DeclCheck() : HasError(false) {}
    bool hasError() { return HasError; }
\end{cpp}

与Parser类中一样,使用一个标志来指示发生了错误。这些名称存储在一个名为Scope的集合中。在包含变量名的Factor节点上,检查变量名是否在集合中:

\begin{cpp}
virtual void visit(Factor &Node) override {
    if (Node.getKind() == Factor::Ident) {
        if (Scope.find(Node.getVal()) == Scope.end())
        error(Not, Node.getVal());
    }
};
\end{cpp}

对于BinaryOp节点,除了两边都存在并访问之外,没什么需要检查:

\begin{cpp}
virtual void visit(BinaryOp &Node) override {
    if (Node.getLeft())
        Node.getLeft()->accept(*this);
    else
        HasError = true;
    if (Node.getRight())
        Node.getRight()->accept(*this);
    else
        HasError = true;
};
\end{cpp}

在WithDecl节点上,填充该集合并开始遍历表达式:

\begin{cpp}
    virtual void visit(WithDecl &Node) override {
        for (auto I = Node.begin(), E = Node.end(); I != E;
            ++I) {
            if (!Scope.insert(*I).second)
            error(Twice, *I);
        }
        if (Node.getExpr())
            Node.getExpr()->accept(*this);
        else
            HasError = true;
    };
};
}
\end{cpp}

semantic()方法只启动树遍历,并返回错误标志:

\begin{cpp}
bool Sema::semantic(AST *Tree) {
    if (!Tree)
        return false;
    DeclCheck Check;
    Tree->accept(Check);
    return Check.hasError();
}
\end{cpp}

还可以做得更多。若没有使用声明的变量,也可以打印警告。我们把这个留给读者当做练习来实现。若语义分析没有错误,则可以从AST生成LLVM IR。这将在下一节中完成。






================================================
FILE: content/part1/chapter2/6.tex
================================================

后端的任务是从模块的LLVM IR中创建优化的机器码。IR是后端接口,可以使用C++接口或以文本形式创建,IR也是从AST生成的。

\mySubsubsection{2.6.1.}{LLVM IR的文本表示}

\begin{enumerate}
\item
向用户询问每个变量的值。

\item
计算表达式的值。

\item
输出结果。
\end{enumerate}

为了让用户提供一个变量的值并打印结果,使用了两个库函数:calc\_read()和calc\_write()。对于with a: 3*a表达式,生成的IR如下所示:

\begin{enumerate}
\item
必须声明库函数,就像在C中一样,语法也类似于C。函数名之前的类型是返回类型。用括号括起来的类型名是参数类型。声明可以出现在文件的任何地方:

\begin{shell}
declare i32 @calc_read(ptr)
declare void @calc_write(i32)
\end{shell}

\item
calc\_read()函数将变量名作为参数。下面的构造定义了一个常量保存a,以及使用空字节作为C字符串的结束符:

\begin{shell}
@a.str = private constant [2 x i8] c"a\00"
\end{shell}

\item
其遵循main()函数。省略参数名,因为没有使用。和C语言一样,函数体用大括号括起来:

\begin{shell}
define i32 @main(i32, ptr) {
\end{shell}

\item
每个基本块必须有一个标签,这是函数的第一个基本块,将其命名为entry:

\begin{shell}
entry:
\end{shell}

\item
calc\_read()用来读取a变量的值。嵌套的getelemenptr指令执行索引计算,以计算指向字符串常量第一个元素的指针。函数结果赋值给未命名的\%2变量。

\begin{shell}
    %2 = call i32 @calc_read(ptr @a.str)
\end{shell}

\item
接下来,将变量乘以3:

\begin{shell}
    %3 = mul nsw i32 3, %2
\end{shell}

\item
结果通过调用calc\_write()函数输出在控制台上:

\begin{shell}
    call void @calc_write(i32 %3)
\end{shell}

\item
最后,main()函数返回0,表示执行成功:

\begin{shell}
    ret i32 0
}
\end{shell}

\end{enumerate}

LLVM IR中的每个值都是有类型的,i32表示32位整数类型,ptr表示指针。

\begin{myNotic}{Note}
以前版本的LLVM使用类型化指针。例如:在LLVM中,指向字节的指针表示为i8*。从LLVM 16开始,不透明指针是默认的。不透明指针只是一个指向内存的指针,不携带关于类型的信息。LLVM IR中的符号是ptr。
\end{myNotic}

既然现在已经清楚了IR的样子,让我们使用AST生成它吧。

\mySubsubsection{2.6.2.}{使用AST生成IR}

CodeGen.h头文件中提供的接口非常少:

\begin{cpp}
#ifndef CODEGEN_H
#define CODEGEN_H

#include "AST.h"

class CodeGen
{
    public:
    void compile(AST *Tree);
};
#endif
\end{cpp}

AST包含了这些信息,所以使用一个访问器来遍历AST。CodeGen.cpp文件的实现如下所示:

\begin{enumerate}
\item
所需的包含在文件的顶部:

\begin{cpp}
#include "CodeGen.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/IR/IRBuilder.h"
#include "llvm/IR/LLVMContext.h"
#include "llvm/Support/raw_ostream.h"
\end{cpp}

\item
LLVM库的命名空间用于名称查找:

\begin{cpp}
using namespace llvm;
\end{cpp}

\item
首先,在访问者中声明一些私有成员。在LLVM中,每个编译单元都由Module类表示,访问者有一个指向模块M的指针。为了便于生成IR,使用了Builder (IRBuilder<>类型)。LLVM有一个类层次结构来表示IR中的类型,可以从LLVM上下文中查找i32等基本类型的实例。

这些基本类型经常使用。为了避免重复查找,这里缓存了所需的类型实例:VoidTy、Int32Ty、PtrTy和Int32Zero。成员V是当前计算的值,通过树遍历更新。最后,nameMap将变量名映射到calc\_read()函数返回的值:

\begin{cpp}
namespace {
class ToIRVisitor : public ASTVisitor {
    Module *M;
    IRBuilder<> Builder;
    Type *VoidTy;
    Type *Int32Ty;
    PointerType *PtrTy;
    Constant *Int32Zero;
    Value *V;
    StringMap<Value *> nameMap;
\end{cpp}

\item
构造函数初始化所有成员:

\begin{cpp}
public:
    ToIRVisitor(Module *M) : M(M), Builder(M->getContext())
    {
        VoidTy = Type::getVoidTy(M->getContext());
        Int32Ty = Type::getInt32Ty(M->getContext());
        PtrTy = PointerType::getUnqual(M->getContext());
        Int32Zero = ConstantInt::get(Int32Ty, 0, true);
    }
\end{cpp}

\item
对于每个函数,必须创建一个FunctionType实例。在C++术语中,这是一个函数原型。函数本身是用函数实例定义的。run()方法首先在LLVM IR中定义main()函数:

\begin{cpp}
    void run(AST *Tree) {
        FunctionType *MainFty = FunctionType::get(
            Int32Ty, {Int32Ty, PtrTy}, false);
        Function *MainFn = Function::Create(
            MainFty, GlobalValue::ExternalLinkage,
            "main", M);
\end{cpp}

\item
然后用entry标签创建BB基本块,并将其添加到IR构建器:

\begin{cpp}
        BasicBlock *BB = BasicBlock::Create(M->getContext(),
                                            "entry", MainFn);
        Builder.SetInsertPoint(BB);
\end{cpp}

\item
准备工作完成后,就可以开始遍历树了:

\begin{cpp}
            Tree->accept(*this);
\end{cpp}

\item
遍历树之后,通过调用calc\_write()函数输出计算值,必须创建函数原型(FunctionType的实例)。唯一的参数是当前值V:

\begin{cpp}
            FunctionType *CalcWriteFnTy =
                FunctionType::get(VoidTy, {Int32Ty}, false);
            Function *CalcWriteFn = Function::Create(
                CalcWriteFnTy, GlobalValue::ExternalLinkage,
                "calc_write", M);
            Builder.CreateCall(CalcWriteFnTy, CalcWriteFn, {V});
\end{cpp}

\item
生成main()函数,并返回0结束:

\begin{cpp}
            Builder.CreateRet(Int32Zero);
        }
\end{cpp}

\item
WithDecl节点保存声明的变量的名称,为calc\_read()函数创建一个函数原型:

\begin{cpp}
    virtual void visit(WithDecl &Node) override {
        FunctionType *ReadFty =
            FunctionType::get(Int32Ty, {PtrTy}, false);
        Function *ReadFn = Function::Create(
            ReadFty, GlobalValue::ExternalLinkage,
            "calc_read", M);
\end{cpp}

\item
该方法循环遍历变量名:

\begin{cpp}
        for (auto I = Node.begin(), E = Node.end(); I != E;
            ++I) {
\end{cpp}

\item
对于每个变量,创建一个带有变量名的字符串:

\begin{cpp}
        StringRef Var = *I;
        Constant *StrText = ConstantDataArray::getString(
            M->getContext(), Var);
        GlobalVariable *Str = new GlobalVariable(
            *M, StrText->getType(),
            /*isConstant=*/true,
            GlobalValue::PrivateLinkage,
            StrText, Twine(Var).concat(".str"));
\end{cpp}

\item
创建调用calc\_read()函数的IR代码,使用上一步中创建的字符串作为参数传递:

\begin{cpp}
        CallInst *Call =
            Builder.CreateCall(ReadFty, ReadFn, {Str});
\end{cpp}

\item
返回值存储在mapNames映射中供以后使用:

\begin{cpp}
        nameMap[Var] = Call;
    }
\end{cpp}

\item
树的遍历将继续使用下面的表达式:

\begin{cpp}
    Node.getExpr()->accept(*this);
};
\end{cpp}

\item
Factor节点可以是变量名,也可以是数字。对于变量名,将在mapNames映射中查找该值。对于数字,将值转换为整数并转换为常数值:

\begin{cpp}
virtual void visit(Factor &Node) override {
    if (Node.getKind() == Factor::Ident) {
        V = nameMap[Node.getVal()];
    } else {
        int intval;
        Node.getVal().getAsInteger(10, intval);
        V = ConstantInt::get(Int32Ty, intval, true);
    }
};
\end{cpp}

\item
对于BinaryOp节点,必须使用正确的计算操作:

\begin{cpp}
virtual void visit(BinaryOp &Node) override {
    Node.getLeft()->accept(*this);
    Value *Left = V;
    Node.getRight()->accept(*this);
    Value *Right = V;
    switch (Node.getOperator()) {
        case BinaryOp::Plus:
        V = Builder.CreateNSWAdd(Left, Right); break;
        case BinaryOp::Minus:
        V = Builder.CreateNSWSub(Left, Right); break;
        case BinaryOp::Mul:
        V = Builder.CreateNSWMul(Left, Right); break;
        case BinaryOp::Div:
        V = Builder.CreateSDiv(Left, Right); break;
    }
};
};
}
\end{cpp}

\item
至此,访问者类就完成了。compile()方法创建全局上下文和模块,运行树遍历,并将生成的IR转储到控制台:

\begin{cpp}
void CodeGen::compile(AST *Tree) {
    LLVMContext Ctx;
    Module *M = new Module("calc.expr", Ctx);
    ToIRVisitor ToIR(M);
    ToIR.run(Tree);
    M->print(outs(), nullptr);
}
\end{cpp}

\end{enumerate}

现在,已经实现了编译器的前端,从读取源代码到生成IR。所有这些组件必须在用户输入时一起工作,这是编译器驱动程序的任务,还需要实现运行时所需的函数。这两个都是下一节的主题。

\mySubsubsection{2.6.3.}{缺失的部分——驱动程序和运行时库}

前几节中的所有阶段都由Calc.cpp驱动程序粘合在一起:声明输入表达式的参数,初始化LLVM,并调用前几节中的所有代码:

\begin{enumerate}
\item
首先,包含所需的头文件:

\begin{cpp}
#include "CodeGen.h"
#include "Parser.h"
#include "Sema.h"
#include "llvm/Support/CommandLine.h"
#include "llvm/Support/InitLLVM.h"
#include "llvm/Support/raw_ostream.h"
\end{cpp}

\item
LLVM自带声明命令行选项的系统,只需要为需要的每个选项声明一个静态变量。在此过程中,该选项会注册到一个全局命令行解析器中。这种方法的优点是,每个组件都可以在需要时添加命令行选项。我们为输入表达式声明一个选项:

\begin{cpp}
static llvm::cl::opt<std::string>
    Input(llvm::cl::Positional,
        llvm::cl::desc("<input expression>"),
        llvm::cl::init(""));
\end{cpp}

\item
main()函数中,首先初始化LLVM库。需要ParseCommandLineOptions()来处理命令行上给出的选项。这还处理帮助信息的输出,若发生错误,使用此方法将结束应用程序的运行:

\begin{cpp}
int main(int argc, const char **argv) {
    llvm::InitLLVM X(argc, argv);
    llvm::cl::ParseCommandLineOptions(
        argc, argv, "calc - the expression compiler\n");
\end{cpp}

\item
接下来,调用词法分析器和解析器。在语法分析之后,检查是否发生了任何错误。若是这种情况,则退出编译器,并返回一个表示失败的错误码:

\begin{cpp}
    Lexer Lex(Input);
    Parser Parser(Lex);
    AST *Tree = Parser.parse();
    if (!Tree || Parser.hasError()) {
        llvm::errs() << "Syntax errors occured\n";
        return 1;
    }
\end{cpp}

\item
若有语义错误,也会这样做:

\begin{cpp}
    Sema Semantic;
    if (Semantic.semantic(Tree)) {
        llvm::errs() << "Semantic errors occured\n";
        return 1;
    }
\end{cpp}

\item
作为驱动程序的最后一步,将调用代码生成器:

\begin{cpp}
    CodeGen CodeGenerator;
    CodeGenerator.compile(Tree);
    return 0;
}
\end{cpp}

\end{enumerate}

现在,已经成功地为用户输入创建了一些IR代码。将目标代码生成委托给LLVM llc静态编译器,这样就完成了编译器的实现。我们将所有组件链接在一起以创建calc应用程序。

运行时库由一个文件rtcalc.c组成,实现了calc\_read()和calc\_write()函数,用C语言实现:

\begin{cpp}
#include <stdio.h>
#include <stdlib.h>

void calc_write(int v)
{
    printf("The result is: %d\n", v);
}
\end{cpp}

calc\_write()只将结果值写入终端:

\begin{cpp}
int calc_read(char *s)
{
    char buf[64];
    int val;
    printf("Enter a value for %s: ", s);
    fgets(buf, sizeof(buf), stdin);
    if (EOF == sscanf(buf, "%d", &val))
    {
        printf("Value %s is invalid\n", buf);
        exit(1);
    }
    return val;
}
\end{cpp}

calc\_read()从终端读取一个整数。不能阻止用户输入字母或其他字符,所以必须仔细检查输入。若输入不是数字,则退出应用程序。更复杂的方法是让用户意识到问题,并再次要求提供一个数字。

下一步是构建并试用我们的编译器calc,这是一个从表达式创建IR的应用程序。

\mySubsubsection{2.6.3.1}{构建和测试calc应用程序}

为了构建calc,首先需要在原始src目录之外创建一个新的CMakeLists.txt文件,其中包含所有源文件实现:

\begin{enumerate}
\item
首先,将最低所需的CMake版本设置为LLVM所需的值,并将项目名称命名为calc:

\begin{cmake}
cmake_minimum_required (VERSION 3.20.0)
project ("calc")
\end{cmake}

\item
接下来,需要加载LLVM包,将LLVM提供的CMake模块目录添加到搜索路径中:

\begin{cmake}
find_package(LLVM REQUIRED CONFIG)
message("Found LLVM ${LLVM_PACKAGE_VERSION}, build type ${LLVM_BUILD_TYPE}")
list(APPEND CMAKE_MODULE_PATH ${LLVM_DIR})
\end{cmake}

\item
还需要从LLVM中添加定义和包含路径,使用的LLVM组件通过函数调用映射到库名:

\begin{cmake}
separate_arguments(LLVM_DEFINITIONS_LIST NATIVE_COMMAND ${LLVM_DEFINITIONS})
add_definitions(${LLVM_DEFINITIONS_LIST})
include_directories(SYSTEM ${LLVM_INCLUDE_DIRS})
llvm_map_components_to_libnames(llvm_libs Core)
\end{cmake}

\item
最后,还需要在构建中包含src子目录,这是本章中所有C++的实现:

\begin{cmake}
add_subdirectory ("src")
\end{cmake}

\end{enumerate}

还需要在src子目录中添加一个新的CMakeLists.txt文件。src目录中的CMake描述如下所示,只需定义可执行文件的名称calc,然后列出要编译的源文件和要链接的库:

\begin{cmake}
add_executable (calc
    Calc.cpp CodeGen.cpp Lexer.cpp Parser.cpp Sema.cpp)
target_link_libraries(calc PRIVATE ${llvm_libs})
\end{cmake}

最后,可以开始构建calc应用程序。在src目录之外,创建一个新的构建目录并对其进行修改,就可以运行CMake配置和构建调用:

\begin{shell}
$ cmake -GNinja -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++
-DLLVM_DIR=<path to llvm installation configuration> ../
$ ninja
\end{shell}

我们现在有了一个新构建的、功能性的calc应用程序,可以生成LLVM IR代码。这可以进一步与llc(LLVM静态后端编译器)一起使用,将IR代码编译成目标文件。

然后,可以使用喜欢的C编译器来链接小型运行时库。在Unix-x86上,可以键入以下命令:

\begin{shell}
$ calc "with a: a*3" | llc –filetype=obj -relocation-model=pic –o=expr.o
$ clang –o expr expr.o rtcalc.c
$ expr
Enter a value for a: 4
The result is: 12
\end{shell}

在其他Unix平台(如AArch64或PowerPC)上,需要删除-relocationmodel=pic选项。

在Windows上,需要使用cl编译器:

\begin{shell}
$ calc "with a: a*3" | llc –filetype=obj –o=expr.obj
$ cl expr.obj rtcalc.c
$ expr
Enter a value for a: 4
The result is: 12
\end{shell}

现在,已经创建了第一个基于llvm的编译器!请花点时间练习不同的表达方式。特别要检查乘法运算符是否在加法运算符之前求值,以及使用括号是否改变了求值顺序(例如四则运算)。













================================================
FILE: content/part1/chapter2/7.tex
================================================
在本章中,了解了编译器的典型组件,使用算术表达式语言介绍编程语言的语法。了解了如何为这种语言开发前端的典型组件:词法分析器、解析器、语义分析器和代码生成器。代码生成器只生成LLVM IR,并使用LLVM llc静态编译器从中创建目标文件。现在我们已经开发了第一个基于llvm的编译器!

下一章中,我们将深化这方面的知识,为编程语言构建前端。

================================================
FILE: content/part1/part1.tex
================================================

将了解如何编译LLVM,并根据需要定制构建;了解LLVM项目是如何组织的,将使用LLVM创建一个项目。最后,将探索编译器的总体结构,并创建一个小型编译器。

\begin{itemize}
\item
第1章,安装LLVM

\item
第2章,编译器的结构
\end{itemize}


================================================
FILE: content/part2/chapter3/0.tex
================================================
正如在前一章了解到的,编译器通常分为两部分——前端和后端。本章中,将实现一种编程语言的前端——主要处理源语言的部分。我们将学习现实世界的编译器使用的技术,并将其应用到我们的编程语言中。

旅程将从定义编程语言的语法开始,并以抽象语法树(AST)结束,其将成为代码生成的基础。对于想要实现编译器的每种编程语言,都可以使用这种方法。

本章中,将了解以下内容:

\begin{itemize}
\item
定义一个真正的编程语言,将了解tinylang语言,它是实际的编程语言的一个子集,将为它实现一个编译器前端

\item
组织编译器项目的目录结构

\item
了解如何为编译器处理多个输入文件

\item
处理用户信息并以令人愉快的方式通知他们问题的技巧

\item
使用模块化部件构建词法分析器

\item
根据从语法导出的规则构造递归下降解析器来执行语法分析

\item
执行语义分析,通过创建AST并分析其特点
\end{itemize}

有了本章的技能,读者们将能够为任何编程语言构建一个编译器前端。










================================================
FILE: content/part2/chapter3/1.tex
================================================
与前一章中简单的calc语言相比,真正的编程面临更多挑战。为了了解其中的细节,我们将在本章和后续章节中使用Modula-2的一小部分。Modula-2设计良好,可选支持泛型和面向对象编程(OOP)。但在本书中,我们并不打算创建一个完整的Modula-2编译器,所以我们将这个子集称为tinylang。

让我们从一个例子开始,看看tinylang编写的程序是什么样的。下面的函数使用欧几里得算法计算最大公约数:

\begin{shell}
MODULE Gcd;

PROCEDURE GCD(a, b: INTEGER) : INTEGER;
VAR t: INTEGER;
BEGIN
    IF b = 0 THEN
        RETURN a;
    END;
    WHILE b # 0 DO
        t := a MOD b;
        a := b;
        b := t;
    END;
    RETURN a;
END GCD;

END Gcd.
\end{shell}

现在我们对使用这种语言编写的程序有了大致的了解,下面来快速了解一下tinylang子集的语法。在接下来的几节中,我们将使用这种语法来派生词法分析器和解析器:

\begin{shell}
compilationUnit
    : "MODULE" identifier ";" ( import )* block identifier "." ;
import : ( "FROM" identifier )? "IMPORT" identList ";" ;
block
    : ( declaration )* ( "BEGIN" statementSequence )? "END" ;
\end{shell}

Modula-2中的编译单元以MODULE关键字开头,后面跟着模块的名称。模块的内容可以包含一个导入模块的列表、声明,以及一个包含在初始化时运行语句的块:

\begin{shell}
declaration
    : "CONST" ( constantDeclaration ";" )*
    | "VAR" ( variableDeclaration ";" )*
    | procedureDeclaration ";" ;
\end{shell}

声明引入了常量、变量和过程。常量的声明以CONST关键字作为前缀。类似地,变量声明以VAR关键字开头。常量的声明非常简单:

\begin{shell}
constantDeclaration : identifier "=" expression ;
\end{shell}

标识符的名称不变。该值是从一个表达式派生的,该表达式必须在编译时可计算。变量的声明会有点复杂:

\begin{shell}
variableDeclaration : identList ":" qualident ;
qualident : identifier ( "." identifier )* ;
identList : identifier ( "," identifier)* ;
\end{shell}

为了能够一次声明多个变量,需要使用标识符列表。类型名可能来自另一个模块,本例中以模块名作为前缀,这称为限定标识符(qualified identifier):

\begin{shell}
procedureDeclaration
    : "PROCEDURE" identifier ( formalParameters )? ";"
        block identifier ;
formalParameters
    : "(" ( formalParameterList )? ")" ( ":" qualident )? ;
formalParameterList
    : formalParameter (";" formalParameter )* ;
formalParameter : ( "VAR" )? identList ":" qualident ;
\end{shell}

前面的代码展示了如何常量、变量声明和过程。过程可以有参数和返回类型,普通参数以值的形式传递,VAR参数以引用的形式传递。块规则中缺少的另一部分是statementSequence,是一个单条语句的列表:

\begin{shell}
statementSequence
    : statement ( ";" statement )* ;
\end{shell}

若语句后面跟着另一个语句,则用分号分隔,所以只支持Modula-2语句的子集:

\begin{shell}
statement
    : qualident ( ":=" expression | ( "(" ( expList )? ")" )? )
    | ifStatement | whileStatement | "RETURN" ( expression )? ;
\end{shell}

该规则的第一部分描述了一个赋值或过程调用。限定标识符后面跟:=是赋值操作。若后面跟着(,则是一个过程调用。其他语句是常见的控制语句:

\begin{shell}
ifStatement
    : "IF" expression "THEN" statementSequence
        ( "ELSE" statementSequence )? "END" ;
\end{shell}

IF语句也有简化的语法,只能有一个ELSE块。使用该语句,可以有条件地保护语句:

\begin{shell}
whileStatement
    : "WHILE" expression "DO" statementSequence "END" ;
\end{shell}

WHILE语句描述了一个由条件保护的循环。与IF语句一起,能够用tinylang编写简单的算法。最后,还缺少了表达式的定义:

\begin{shell}
expList
    : expression ( "," expression )* ;
expression
    : simpleExpression ( relation simpleExpression )? ;
relation
    : "=" | "#" | "<" | "<=" | ">" | ">=" ;
simpleExpression
    : ( "+" | "-" )? term ( addOperator term )* ;
addOperator
    : "+" | "-" | "OR" ;
term
    : factor ( mulOperator factor )* ;
mulOperator
    : "*" | "/" | "DIV" | "MOD" | "AND" ;
factor
    : integer_literal | "(" expression ")" | "NOT" factor
    | qualident ( "(" ( expList )? ")" )? ;
\end{shell}

表达式语法与前一章中的calc非常相似,只支持INTEGER和BOOLEAN数据类型。

此外,还使用了标识符和integer\_literal标记。标识符是以字母或下划线开头,后面跟着字母、数字和下划线的名称。整数字面值是一个十进制数字序列或后跟字母H的十六进制数字序列。

这些规则已经有很多了,我们只介绍Modula-2的一部分!不过,也可以在这个子集中编写小型应用程序。来实现一个tinylang编译器吧!


================================================
FILE: content/part2/chapter3/2.tex
================================================
tinylang的目录结构遵循我们在第1章安装LLVM中列出的方式。每个组件的源代码位于lib目录的子目录中,头文件位于include/tinylang的子目录中。子目录以组件命名。在第1章安装LLVM中,我们只创建了基本组件。

前一章中,了解了需要实现词法分析器、解析器、AST和语义分析器。每个都是自己的组件,分别称为Lexer、Parser、AST和Sema。本章将要使用的目录布局是这样的:

\myGraphic{0.3}{content/part2/chapter3/images/1.png}{图3.1 - tinylang项目的目录布局}

组件具有明确定义的依赖关系。Lexer只依赖于Basic,Parser依赖于Basic、Lexer、AST和Sema,Sema只依赖于Basic和AST。定义良好的依赖关系有助于我们重用组件。

来仔细看看实现吧!


================================================
FILE: content/part2/chapter3/3.tex
================================================
真正的编译器必须处理许多文件,开发人员用主编译单元的名称来调用编译器。这个编译单元可以引用其他文件——例如,通过C语言中的\#include指令或Python或Modula-2中的import语句。导入的模块可以导入其他模块,以此类推。所有这些文件必须加载到内存中,并通过编译器的分析阶段运行。开发过程中,开发人员可能会犯语法或语义错误。当检测到错误时,应该打印一条错误消息,包括源代码行和一个标记,这个基本组成部分很重要。

幸运的是,LLVM提供了一个解决方案:LLVM::SourceMgr类。通过调用AddNewSourceBuffer()方法,一个新的源文件添加到SourceMgr中。或者,通过调用AddIncludeFile()方法来加载文件。这两种方法都返回一个标识缓冲区的ID。可以使用这个ID来取得一个指向相关文件的内存缓冲区的指针。要在文件中定义位置,可以使用llvm::SMLoc类。该类封装了一个指向缓冲区的指针,各种PrintMessage()方法允许向用户发送错误和其他信息消息。

================================================
FILE: content/part2/chapter3/4.tex
================================================
我们目前还缺少消息的集中定义。在大型软件(如编译器)中,你应该不会希望消息字符串散布各处。若有修改消息或将其翻译成另一种语言的请求,最好将它们放在一个中心位置!一种简单的方法是,每个消息都有一个ID(枚举成员)、一个严重级别(如Error或Warning)和一个包含消息的字符串。在代码中,只需要引用消息ID即可。

严重性级别和消息字符串仅在消息输出时使用,必须一致地管理这三项(ID、安全级别和消息)。LLVM库使用预处理器来解决这个问题,数据存储在一个后缀为.def的文件中,并包装在一个宏名称中。该文件通常被包含多次,并对该宏有不同的定义。该定义位于include/tinylang/Basic/Diagnostic.def文件路径中:

\begin{cpp}
#ifndef DIAG
#define DIAG(ID, Level, Msg)
#endif

DIAG(err_sym_declared, Error, "symbol {0} already declared")

#undef DIAG
\end{cpp}

第一个宏参数ID是枚举标签,第二个参数Level是严重性,第三个参数Msg是消息文本。有了这个定义,就可以定义一个diagnosticengine类来发出错误消息。接口在include/tinylang/ Basic/Diagnostic.h文件中:

\begin{cpp}
#ifndef TINYLANG_BASIC_DIAGNOSTIC_H
#define TINYLANG_BASIC_DIAGNOSTIC_H

#include "tinylang/Basic/LLVM.h"
#include "llvm/ADT/StringRef.h"
#include "llvm/Support/FormatVariadic.h"
#include "llvm/Support/SMLoc.h"
#include "llvm/Support/SourceMgr.h"
#include "llvm/Support/raw_ostream.h"
#include <utility>

namespace tinylang {
\end{cpp}

在包含必要的头文件之后,可以使用Diagnostic.def定义枚举。为了不污染全局命名空间,我们使用了名为diag的嵌套命名空间:

\begin{cpp}
namespace diag {
enum {
#define DIAG(ID, Level, Msg) ID,
#include "tinylang/Basic/Diagnostic.def"
};
} // namespace diag
\end{cpp}

DiagnosticsEngine类使用SourceMgr实例通过report()方法发送消息。消息可以有参数。为了实现这个功能,我们使用了LLVM提供的可变格式支持。消息文本和严重性级别在静态方法的帮助下检索,还会统计发出的错误消息的数量:

\begin{cpp}
class DiagnosticsEngine {
    static const char *getDiagnosticText(unsigned DiagID);
    static SourceMgr::DiagKind
    getDiagnosticKind(unsigned DiagID);
\end{cpp}

消息字符串由getDiagnosticText()返回,而级别由getDiagnosticKind()返回。这两个方法稍后会在.cpp文件中进行实现:

\begin{cpp}
SourceMgr &SrcMgr;
unsigned NumErrors;

public:
    DiagnosticsEngine(SourceMgr &SrcMgr)
        : SrcMgr(SrcMgr), NumErrors(0) {}
    unsigned nunErrors() { return NumErrors; }
\end{cpp}

由于消息可以具有可变数量的参数,所以C++中的解决方案是使用可变模板。当然,LLVM提供的formatv()函数也会使用这种方法。要获得格式化的消息,只需要转发模板参数即可:

\begin{cpp}
    template <typename... Args>
    void report(SMLoc Loc, unsigned DiagID,
                Args &&... Arguments) {
        std::string Msg =
            llvm::formatv(getDiagnosticText(DiagID),
        std::forward<Args>(Arguments)...)
        .str();
        SourceMgr::DiagKind Kind = getDiagnosticKind(DiagID);
        SrcMgr.PrintMessage(Loc, Kind, Msg);
        NumErrors += (Kind == SourceMgr::DK_Error);
    }
};

} // namespace tinylang
#endif
\end{cpp}

至此,我们已经实现了类的大部分内容,只缺失了getDiagnosticText()和getDiagnosticKind()。它们在lib/Basic/Diagnostic.cpp文件中定义,并使用了Diagnostic.def文件:

\begin{cpp}
#include "tinylang/Basic/Diagnostic.h"

using namespace tinylang;

namespace {
const char *DiagnosticText[] = {
    #define DIAG(ID, Level, Msg) Msg,
    #include "tinylang/Basic/Diagnostic.def"
};
\end{cpp}

与头文件中一样,定义DIAG宏来检索所需的部件。这里,定义了一个保存文本消息的数组。因此,DIAG宏只返回Msg部分。我们对于level中使用了相同的方法:

\begin{cpp}
SourceMgr::DiagKind DiagnosticKind[] = {
#define DIAG(ID, Level, Msg) SourceMgr::DK_##Level,
include "tinylang/Basic/Diagnostic.def"
};
} // namespace
\end{cpp}

毫不奇怪,这两个函数都只是对数组进行索引,并返回所需的数据:

\begin{cpp}
const char *
DiagnosticsEngine::getDiagnosticText(unsigned DiagID) {
    return DiagnosticText[DiagID];
}

SourceMgr::DiagKind
DiagnosticsEngine::getDiagnosticKind(unsigned DiagID) {
    return DiagnosticKind[DiagID];
}
\end{cpp}

SourceMgr和DiagnosticsEngine类的组合为其他组件提供了良好的基础。我们将首先在lexer中使用它们!


================================================
FILE: content/part2/chapter3/5.tex
================================================
正如从前一章所知,我们需要一个Token类和一个Lexer类,所以需要TokenKind枚举来为每个Token类提供唯一的编号。拥有一个集所有功能于一体的头文件和实现文件并不能扩展,TokenKind可以普遍使用,并放置在Basic组件中。Token和Lexer类属于Lexer组件,但位于不同的头文件和实现文件中。

有三种不同类型的标记:关键字、标点符号和标记,它们表示许多值的集合。例如CONST关键字,;分隔符和ident标记,它们分别表示源代码中的标识符。每个标记都需要枚举的成员名。关键字和标点符号具有可以用于消息的自然显示名称。

与许多编程语言一样,关键字是标识符的子集。要将标记分类为关键字,我们需要一个关键字过滤器,检查找到的标识符是否确实是关键字。这与C或C++中的行为相同,其中关键字也是标识符的子集。编程语言不断发展,可能会引入新的关键字。例如,最初的K\&R C语言没有用enum关键字定义枚举,所以应该出现一个标志来指示关键字的语言级别。

我们收集了几条信息,都属于TokenKind枚举的一个成员:枚举成员的标签、标点符号的拼写和关键字的标志。对于诊断消息,我们将信息集中存储在一个名为include/tinylang/Basic/tokenkind.def的.def文件中,该文件如下所示。需要注意的一点是,其关键字的前缀是kw\_:

\begin{cpp}
#ifndef TOK
#define TOK(ID)
#endif
#ifndef PUNCTUATOR
#define PUNCTUATOR(ID, SP) TOK(ID)
#endif
#ifndef KEYWORD
#define KEYWORD(ID, FLAG) TOK(kw_ ## ID)
#endif

TOK(unknown)
TOK(eof)
TOK(identifier)
TOK(integer_literal)

PUNCTUATOR(plus, "+")
PUNCTUATOR(minus, "-")
// …

KEYWORD(BEGIN , KEYALL)
KEYWORD(CONST , KEYALL)
// …

#undef KEYWORD
#undef PUNCTUATOR
#undef TOK
\end{cpp}

有了这些集中的定义,就可以很容易地在include/tinylang/Basic/TokenKind.h文件中创建TokenKind枚举,枚举也会有自己的命名空间,例如:

\begin{cpp}
#ifndef TINYLANG_BASIC_TOKENKINDS_H
#define TINYLANG_BASIC_TOKENKINDS_H
namespace tinylang {
    namespace tok {
        enum TokenKind : unsigned short {
#define TOK(ID) ID,
#include "TokenKinds.def"
        NUM_TOKENS
    };
\end{cpp}

现在应该熟悉填充数组的模式了,TOK宏定义为只返回ID。另外,还将NUM\_TOKENS定义为枚举的最后一个成员,用于表示已定义的标记的数量:

\begin{cpp}
        const char *getTokenName(TokenKind Kind);
        const char *getPunctuatorSpelling(TokenKind Kind);
        const char *getKeywordSpelling(TokenKind Kind);
    }
}

#endif
\end{cpp}

实现文件lib/Basic/tokenkind.cpp也使用.def文件来检索名称:

\begin{cpp}
#include "tinylang/Basic/TokenKinds.h"
#include "llvm/Support/ErrorHandling.h"

using namespace tinylang;

static const char * const TokNames[] = {
    #define TOK(ID) #ID,
    #define KEYWORD(ID, FLAG) #ID,
    #include "tinylang/Basic/TokenKinds.def"
    nullptr
};
\end{cpp}

标记的文本名称源自其枚举标签ID。有两个特点:

\begin{itemize}
\item
首先,一些操作符共享相同的前缀,例如<和<=。若当前查看的字符是<,则必须先检查下一个字符,再决定找到哪个标记。请记住,输入需要以空字节结束,若当前字符有效,则可以使用下一个字符:

\begin{cpp}
    case '<':
        if (*(CurPtr + 1) == '=')
            formTokenWithChars(token, CurPtr + 2,
                               tok::lessequal);
        else
            formTokenWithChars(token, CurPtr + 1, tok::less);
        break;
\end{cpp}

\item
另一个是,现在有更多的关键字。我们该如何处理?一个简单而快速的解决方案是用关键字填充散列表,这些关键字都存储在tokenkind.def文件中,可以在Lexer类的实例化期间完成。使用这种方法,可以使用附加标志过滤关键字,还可以支持不同级别的语言。这里,还不需要这种灵活性。头文件中,关键字过滤器定义如下,使用llvm::StringMap实例作为哈希表:

\begin{cpp}
    class KeywordFilter {
        llvm::StringMap<tok::TokenKind> HashTable;
        void addKeyword(StringRef Keyword,
                        tok::TokenKind TokenCode);
    public:
        void addKeywords();
\end{cpp}

getKeyword()返回给定字符串的标记类型,若字符串不代表关键字则返回默认值:

\begin{cpp}
    tok::TokenKind getKeyword(
            StringRef Name,
            tok::TokenKind DefaultTokenCode = tok::unknown) {
        auto Result = HashTable.find(Name);
        if (Result != HashTable.end())
            return Result->second;
        return DefaultTokenCode;
    }
};
\end{cpp}

实现文件中,填充关键字表:

\begin{cpp}
void KeywordFilter::addKeyword(StringRef Keyword,
                               tok::TokenKind TokenCode) {
    HashTable.insert(std::make_pair(Keyword, TokenCode));
}

void KeywordFilter::addKeywords() {
#define KEYWORD(NAME, FLAGS) \
    addKeyword(StringRef(#NAME), tok::kw_##NAME);
#include "tinylang/Basic/TokenKinds.def"
}
\end{cpp}

\end{itemize}

使用刚学到的技术,编写一个高效的词法分析器类并不困难。由于编译速度很重要,许多编译器使用手写词法分析器,其中一个例子就是clang。


================================================
FILE: content/part2/chapter3/6.tex
================================================
如前一章所示,解析器是从语法派生出来的。回顾一下所有的构造规则,对于语法的每个规则,创建一个以规则左侧的非终结符命名的方法来解析规则的右侧。根据右边的定义,可以有以下操作:

\begin{itemize}
\item
对于每个非终结符,调用相应的方法

\item
每个标记都要进行处理

\item
对于备选项和可选的或重复的组,将检查超前标记(下一个未使用的令牌),以决定在何处继续
\end{itemize}

让我们将这些结构规则应用于以下语法规则:

\begin{shell}
ifStatement
    : "IF" expression "THEN" statementSequence
        ( "ELSE" statementSequence )? "END" ;
\end{shell}

可以很容易地将其转换为下面的C++代码:

\begin{cpp}
void Parser::parseIfStatement() {
    consume(tok::kw_IF);
    parseExpression();
    consume(tok::kw_THEN);
    parseStatementSequence();
    if (Tok.is(tok::kw_ELSE)) {
        advance();
        parseStatementSequence();
    }
    consume(tok::kw_END);
}
\end{cpp}

tinylang的整个语法可以通过这种方式转换成C++。因为在互联网上找到的大多数语法都不适合这种结构,所以必须小心避免一些陷阱。

\begin{myTip}{语法和解析器}
有两种不同类型的解析器:自顶向下解析器和自底向上解析器。其名称来源于解析过程中处理规则的顺序,解析器的输入是词法分析器生成的标记序列。

自顶向下解析器展开规则中最左边的符号,直到匹配到一个标记。若使用了所有标记并展开了所有符号,则解析成功。这正是tinylang解析器的工作方式。

自底而上的解析器则相反:查看标记序列,并尝试用规则左侧的语法符号替换这些标记。例如,接下来的标记是if、3、+和4,自下而上的解析器将用expression符号替换3 + 4标记,从而产生IF expression序列。当检测到一系列属于完整IF语句的标记时,这个标记和符号序列将使用ifStatement符号替换。

若使用了所有标记,并且只剩下开始符号,则解析成功。虽然自顶向下的解析器可以很容易地手工构造,但自底向上的解析器却不是这样。

描述这两种解析器的另一种方法是首先展开符号。两者都从左到右读取输入,但是自顶向下的解析器首先展开最左边的符号,而自底向上的解析器首先展开最右边的符号。因此,自顶向下的解析器也称为LL解析器,而自底向上的解析器称为LR解析器。

语法必须具有某些属性,才可以从中派生出LL或LR解析器。语法是相应地命名的:需要一个LL语法来构造LL解析器。

可以在大学教科书中找到有关编译器构造的更多细节,例如Wilhelm, Seidl和Hack合著的《Compiler Design. Syntactic and Semantic Analysis》,Springer 2013,以及Grune和Jacobs合著的《Parsing Techniques, A practical guide》,Springer 2008。
\end{myTip}

需要查找的一个问题是左递归规则。若规则的右侧以与左侧相同的终端开始,则称为左递归规则。典型的例子可以在表达式语法中看到:

\begin{shell}
expression : expression "+" term ;
\end{shell}

若从语法上看还不清楚,翻译成的C++会很明显地有无限递归:

\begin{cpp}
void Parser::parseExpression() {
    parseExpression();
    consume(tok::plus);
    parseTerm();
}
\end{cpp}

左递归也可以间接发生,并且涉及更多规则,这更难以发现。这就是为什么会存在一种可以检测和消除左递归的算法。

\begin{myNotic}{Note}
左递归规则只是LL解析器的问题,比如tinylang的递归下降解析器。原因是这些解析器首先展开最左边的符号。相反,若使用解析器生成器生成LR解析器,首先展开最右边的符号,则应该避免右递归规则。
\end{myNotic}

每一步中,解析器仅仅通过提前查看后续的标记来决定如何继续解析。若不能确定地做出这个决定,则代表语法有冲突。为了说明这一点,看一下C\#中的using语句。与C++一样,using语句可用于使符号在命名空间中可见,例如using Math;。也可以用using M = Math;为导入的符号定义别名。在语法中,这可以表示为:

\begin{shell}
usingStmt : "using" (ident "=")? ident ";"
\end{shell}

这里有一个问题:在解析器使用using关键字之后,预检标记是ident类型的,但这些信息不足以让我们决定是否跳过或解析中间的可选组。若可选组开始使用的标记集与可选组后面的标记集重叠,就会出现这种情况。

让我们用另一种不使用可选组的方法来重写这个规则:

\begin{shell}
usingStmt : "using" ( ident "=" ident | ident ) ";" ;
\end{shell}

现在,有一个不同的冲突:两种选择都以相同的标记开始。解析器只查看预检标记,无法确定哪个选项是正确的。

这些冲突很常见,知道如何处理它们就好。一种方法是以消除冲突的方式重写语法。在前面的示例中,两种备选方案都以相同的标记开头,我们可以把此标记提取出来,得到以下规则:

\begin{shell}
usingStmt : "using" ident ("=" ident)? ";" ;
\end{shell}

这种表述没有冲突,但也应该注意到它的表达不够直观。在另外两个公式中,很明显哪个标识是别名,哪个标识是命名空间名。在无冲突规则中,最左边的标识的角色可能会改变。一开始,它是命名空间的名称,但若后面跟着等号,它就变成了别名。

第二种方法是添加一个谓词来区分这两种情况。该谓词通常称为解析器(resolver),可以使用上下文信息进行决策(例如在符号表中查找名称),也可以查看多个标记。假设词法分析器有一个名为Token \&peek(int n)的方法,该方法在当前的超前标记之后返回第n个标记。这里,等号的存在性可以作为判断过程中的额外谓词:

\begin{cpp}
if (Tok.is(tok::ident) && Lex.peek(0).is(tok::equal)) {
    advance();
    consume(tok::equal);
}
consume(tok::ident);
\end{cpp}

第三种方法是使用回溯。为此,需要保存当前状态,并且尝试解析冲突的组。若这没有成功,需要返回到保存的状态并尝试另一个路径。这里搜索的是要应用的正确规则,效率不如其他方法。因此,只能在万不得已的情况下才会使用这种方法。

现在,让我们加入错误恢复。上一章中,介绍了一种称为恐慌模式(panic mode)的错误恢复技术。基本思想是跳过标记,直到找到一个适合继续解析的标记。例如,在tinylang中,语句后跟分号(:)。

若If语句中存在语法问题,则跳过所有标记,直到找到分号为止,再继续执行下一个语句。与其使用临时定义的标记集,不如使用系统的方法。

对于每个非终结符,计算可以跟在该非终结符后面的标记集合,称为跟随集(FOLLOW Set)。对于语句非终结符,后面可以是;、ELSE和END标记,所以必须在parseStatement()的错误恢复部分使用这个集。这个方法假定语法错误可以就地处理,但大多数情况下这是不可能的。解析器可能会跳过很多标记,直到到达输入的结尾。在这种情况下,无法进行就地恢复。

为了防止无意义的错误消息,需要通知调用方法错误恢复仍未完成。这可以通过bool来实现。若返回true,则表示错误恢复尚未完成,而false则表示解析(包括可能的错误恢复)成功。

有许多方法可以扩展此错误恢复方案。使用主动调用者的FOLLOW集合是一种常见的方法。作为一个简单的例子,假设parseStatement()由parseStatementSequence()调用,而parseBlock()本身又由parseModule()调用。

这里,每个相应的非终结符都有一个FOLLOW集合。若解析器在parseStatement()中检测到语法错误,则跳过标记,直到当前标记出现在至少一个主动调用者的FOLLOW集合中。若标记位于当前语句的FOLLOW集合中,则就地恢复错误,并向调用者返回false;否则,返回true,表示必须继续进行错误恢复。这个扩展的一种可能的实现策略是将std::bitset或std::tuple传递给调用方,以表示当前FOLLOW集合的并集。

最后一个问题仍然没有解决:我们如何调用错误恢复?上一章中,goto用于跳转到错误恢复块。这是可行的,但不够好。根据前面讨论的内容,我们可以在单独的方法中跳过标记。Clang有一个用于此目的的方法skipUntil(),tinylang也可以用。

因为下一步是向解析器添加语义操作,所以若有必要的话,最好有一个中央位置放置清理代码。嵌套函数将是理想的选择。C++没有嵌套函数,而Lambda函数可以达到类似的目的。我们最初看到的parseIfStatement()方法在添加完整的错误恢复代码时,类似如下实现:

\begin{cpp}
bool Parser::parseIfStatement() {
    auto _errorhandler = [this] {
        return skipUntil(tok::semi, tok::kw_ELSE, tok::kw_END);
    };
    if (consume(tok::kw_IF))
        return _errorhandler();
    if (parseExpression(E))
        return _errorhandler();
    if (consume(tok::kw_THEN))
        return _errorhandler();
    if (parseStatementSequence(IfStmts))
        return _errorhandler();
    if (Tok.is(tok::kw_ELSE)) {
        advance();
        if (parseStatementSequence(ElseStmts))
        return _errorhandler();
    }
    if (expect(tok::kw_END))
        return _errorhandler();
    return false;
}
\end{cpp}

\begin{myTip}{解析器和词法分析器生成器}
手动构造解析器和词法分析器可能是一项乏味的任务,特别是在尝试发明一种新的编程语言,并经常更改语法的情况下。幸运的是,有些工具可以自动完成这项任务。

经典的Linux工具是flex(\url{https://github.com/westes/flex})和bison (\url{https://www.gnu.org/software/bison/})。flex从一组正则表达式生成词法分析器,而bison从语法描述生成LALR(1)解析器。这两个工具都可以生成C/C++源代码,并且可以一起使用。

另一个流行的工具是AntLR(\url{https://www.antlr.org/})。AntLR可以从语法描述生成词法分析器、解析器和AST。生成的解析器属于LL(*)类,所以它是一个自顶向下的解析器,使用可变数量的提前查找量(lookahead)来解决冲突。该工具是用Java编写的,但可以生成许多流行语言的源代码,包括C/C++。

所有这些工具都需要一些库支持。若正在寻找生成自包含的词法分析器和解析器的工具,那么Coco/R(\url{https://ssw.jku.at/Research/Projects/Coco/})可能是适合您的工具。Coco/R根据LL(1)语法描述生成词法分析器和递归下降解析器,类似于本书中使用的语法描述。生成的文件基于模板文件,可以根据需要更改模板文件。该工具用C\#编写的,可以移植到C++、Java和其他语言。

还有许多其他可用的工具,其在特性和支持的输出语言方面差异很大。在选择工具时,也需要考虑权衡。LALR(1)解析器生成器(如bison)可以接受范围广泛的语法,可以在互联网上找到的免费语法通常是LALR(1)语法。

缺点是,这些生成器生成需要在运行时解释的状态机,这可能比递归下降解析器慢,错误处理也更加复杂。Bison对处理语法错误提供了基本支持,但要正确使用它,需要对解析器的工作原理有深入的了解。与此相比,AntLR消耗的语法类略小,但会自动生成错误处理,并且还可以生成AST,所以重写语法以便与AntLR一起使用可能会加快之后的开发进度。
\end{myTip}


================================================
FILE: content/part2/chapter3/7.tex
================================================

我们在上一节中构建的解析器只检查输入的语法,下一步是添加执行语义分析的能力。上一章的calc示例中,解析器构建了一个AST。在另一个单独的阶段中,语义分析器在这个树上工作。这种方法总是可以使用的。本节中,我们将使用一种稍微不同的方法,并将解析器和语义分析器更多地交织在一起。

语义分析器需要做什么?一起来看看:

\begin{itemize}
\item
对于每个声明,必须检查变量、对象等的名称,以确保它们没有在其他地方声明过。

\item
对于表达式或语句中每次出现的名称,必须检查是否声明了该名称,以及所需的用途是否符合声明。

\item
对于每个表达式,必须计算结果类型。还需要计算表达式是否为常量,若是常量,具为哪个值。

\item
对于赋值和形参传递,必须检查类型是否兼容。此外,必须检查IF和WHILE语句中的条件是否为BOOLEAN类型。
\end{itemize}

对于这样一个编程语言的小子集来说,要检查的东西已经很多了!

\mySubsubsection{3.7.1.}{处理名称作用域}

先来了解一下名称作用域,名称作用域是该名称可见的范围。与C语言一样,tinylang使用了使用前声明(declare-before-use)模型。例如,B和X变量在模块级别可声明为INTEGER类型:

\begin{shell}
VAR B, X: INTEGER;
\end{shell}

声明之前,变量是未知的,不能使用。这只有在声明之后才有可能。在一个过程中,可以声明更多的变量:

\begin{shell}
PROCEDURE Proc;
VAR B: BOOLEAN;
BEGIN
    (* Statements *)
END Proc;
\end{shell}

注释所在的位置,使用B指向B局部变量,而使用X指向X全局变量。局部变量B的作用域是Proc。若在当前作用域中找不到名称,则在外层封闭作用域中继续搜索,所以X变量可以在过程中使用。在tinylang中,只有模块和过程打开一个新的作用域。其他语言结构,如结构和类,通常也会打开作用域。预定义实体,如INTEGER类型和TRUE在全局作用域中声明,包含模块的作用域。

在tinylang中,只有名字才是关键。可以将作用域实现为从名称到其声明的映射,只有在新名称不存在的情况下才能插入该名称。对于查找,还必须知道封闭或父作用域,接口(在include/tinylang/Sema/Scope.h文件中)如下所示:

\begin{cpp}
#ifndef TINYLANG_SEMA_SCOPE_H
#define TINYLANG_SEMA_SCOPE_H

#include "tinylang/Basic/LLVM.h"
#include "llvm/ADT/StringMap.h"
#include "llvm/ADT/StringRef.h"

namespace tinylang {

class Decl;

class Scope {
    Scope *Parent;
    StringMap<Decl *> Symbols;

    public:
    Scope(Scope *Parent = nullptr) : Parent(Parent) {}

    bool insert(Decl *Declaration);
    Decl *lookup(StringRef Name);

    Scope *getParent() { return Parent; }
};
} // namespace tinylang
#endif
\end{cpp}

lib/Sema/Scope.cpp文件中的实现如下所示:

\begin{cpp}
#include "tinylang/Sema/Scope.h"
#include "tinylang/AST/AST.h"

using namespace tinylang;

bool Scope::insert(Decl *Declaration) {
    return Symbols
        .insert(std::pair<StringRef, Decl *>(
            Declaration->getName(), Declaration))
        .second;
}
\end{cpp}

注意,StringMap::insert()方法不会覆盖现有条目。结果std::pair的第二个成员表示表是否更新,此信息将返回给调用者。

要实现对符号声明的搜索,lookup()方法在当前作用域内搜索,若没有找到,则搜索父成员链接的作用域:

\begin{cpp}
Decl *Scope::lookup(StringRef Name) {
    Scope *S = this;
    while (S) {
        StringMap<Decl *>::const_iterator I =
            S->Symbols.find(Name);
        if (I != S->Symbols.end())
            return I->second;
        S = S->getParent();
    }
    return nullptr;
}
\end{cpp}

然后,对变量声明进行如下处理:

\begin{itemize}
\item
当前的作用域是模块作用域。

\item
查找INTEGER类型声明。若没有找到声明或者不是类型声明,则会出现错误。

\item
实例化一个名为VariableDeclaration的新AST节点,其中重要的属性是名称B和类型。

\item
名称B插入到当前作用域,映射到声明实例。若该名称已经存在于作用域中,则这是一个错误,所以当前作用域的内容不会改变。

\item
对于X变量也是如此。
\end{itemize}

这里执行两个任务。与calc示例一样,构造AST节点。同时,计算节点的属性,如类型。为什么这是可能的呢?

语义分析器可以依赖于两组不同的属性:作用域继承自调用者,类型声明可以通过评估类型声明中的名称来计算(或者说合成)。该语言的设计方式,使得这两组属性足以计算AST节点的所有属性。

这里,需要先声明再使用模型。若一种语言允许在声明之前使用名称,例如C++中类中的成员,不可能一次计算一个AST节点的所有属性,AST节点必须仅使用部分计算的属性或仅使用普通信息(例如在calc示例中)来构造。

然后必须访问AST一次或多次以确定缺失的信息。在tinylang(和Modula-2)的情况下,可以省去AST构造——AST是通过parseXXX()方法的调用层次结构间接表示。使用AST生成代码更为常见,所以我们在这里也构造一个AST。

在把这些部分放在一起之前,我们需要了解LLVM使用运行时类型信息(runtime type information, RTTI)的风格。

\mySubsubsection{3.7.2.}{使用LLVM风格RTTI的AST}

当然,AST节点是类层次结构的一部分。声明总是有一个名称,其他属性取决于所声明的内容。若声明了变量,则需要指定类型。声明常量需要类型、值等。在运行时,需要找出使用的是哪种类型的声明,dynamic\_cast<>的C++操作符可用于此。问题是,只有当C++类附加了虚函数表时,所需的RTTI才可用——使用了虚函数。另一个缺点是C++ RTTI过于臃肿。为了避免这些缺点,LLVM开发人员引入了一种自制的RTTI风格,这种风格在整个LLVM库中使用。

我们的层次结构的(抽象)基类是Decl,要实现llvm风格的RTTI,必须添加一个公共枚举,其中包含每个子类的标签。此外,还需要该类型的私有成员和公共getter。私有成员通常为Kind。具体的实现,如下所示:

\begin{cpp}
class Decl {
public:
    enum DeclKind { DK_Module, DK_Const, DK_Type,
        DK_Var, DK_Param, DK_Proc };
private:
    const DeclKind Kind;
public:
    DeclKind getKind() const { return Kind; }
};
\end{cpp}

现在,每个子类都需要一个名为classof的特殊函数成员。该函数的目的是确定给定实例是否属于所请求的类型。对于VariableDeclaration,实现如下:

\begin{cpp}
static bool classof(const Decl *D) {
    return D->getKind() == DK_Var;
}
\end{cpp}

现在,可以使用特殊模板llvm::isa<>来检查对象是否为所请求的类型,并使用llvm::dyn\_cast<>来动态转换对象。存在更多模板,但这两个是最常用的模板。有关其他模板,请参阅\url{https://llvm.org/docs/ProgrammersManual.html#the-isa-cast-and-dyn-cast-templates};有关LLVM样式的更多信息,包括更高级的用法,请参阅\url{https://llvm.org/docs/HowToSetUpLLVMStyleRTTI.html}。

\mySubsubsection{3.7.3.}{创建语义分析器}

有了这些,现在可以实现所有的部分。首先,必须为存储在include/llvm/tinylang/AST/AST.h文件中的变量创建AST节点的定义。除了支持LLVM风格的RTTI,基类存储声明的名称、名称的位置和一个指向外层声明的指针,后者在嵌套过程的代码生成过程中是必需的。Decl基类声明如下:

\begin{cpp}
class Decl {
public:
    enum DeclKind { DK_Module, DK_Const, DK_Type,
                    DK_Var, DK_Param, DK_Proc };

private:
    const DeclKind Kind;

protected:
    Decl *EnclosingDecL;
    SMLoc Loc;
    StringRef Name;

public:
    Decl(DeclKind Kind, Decl *EnclosingDecL, SMLoc Loc,
        StringRef Name)
        : Kind(Kind), EnclosingDecL(EnclosingDecL), Loc(Loc),
        Name(Name) {}

    DeclKind getKind() const { return Kind; }
    SMLoc getLocation() { return Loc; }
    StringRef getName() { return Name; }
    Decl *getEnclosingDecl() { return EnclosingDecL; }
};
\end{cpp}

变量的声明只向类型声明添加一个指针:

\begin{cpp}
class TypeDeclaration;

class VariableDeclaration : public Decl {
    TypeDeclaration *Ty;

public:
    VariableDeclaration(Decl *EnclosingDecL, SMLoc Loc,
                        StringRef Name, TypeDeclaration *Ty)
        : Decl(DK_Var, EnclosingDecL, Loc, Name), Ty(Ty) {}

    TypeDeclaration *getType() { return Ty; }

    static bool classof(const Decl *D) {
        return D->getKind() == DK_Var;
    }
};
\end{cpp}

解析器中的方法需要扩展语义动作和收集信息的变量:

\begin{cpp}
bool Parser::parseVariableDeclaration(DeclList &Decls) {
    auto _errorhandler = [this] {
        while (!Tok.is(tok::semi)) {
            advance();
            if (Tok.is(tok::eof)) return true;
        }
        return false;
    };

    Decl *D = nullptr; IdentList Ids;
    if (parseIdentList(Ids)) return _errorhandler();
    if (consume(tok::colon)) return _errorhandler();
    if (parseQualident(D)) return _errorhandler();
    Actions.actOnVariableDeclaration(Decls, Ids, D);
    return false;
}
\end{cpp}

DeclList是声明列表,std::vector<Decl*>和IdentList是位置和标识符列表,std::vector<std::pair<SMLoc, StringRef>{}>。

parseQualident()方法返回一个声明,在本例中应该是一个类型声明。

解析器类知道存储在Actions成员中的语义分析器类Sema的实例。对actOnVariableDeclaration()的调用运行语义分析器和AST构造,实现在lib/Sema/Sema.cpp文件中:

\begin{cpp}
void Sema::actOnVariableDeclaration(DeclList &Decls,
IdentList &Ids,
Decl *D) {
    if (TypeDeclaration *Ty = dyn_cast<TypeDeclaration>(D)) {
        for (auto &[Loc, Name] : Ids) {
            auto *Decl = new VariableDeclaration(CurrentDecl, Loc,
            Name, Ty);
            if (CurrentScope->insert(Decl))
                Decls.push_back(Decl);
            else
                Diags.report(Loc, diag::err_symbold_declared, Name);
        }
    } else if (!Ids.empty()) {
        SMLoc Loc = Ids.front().first;
        Diags.report(Loc, diag::err_vardecl_requires_type);
    }
}
\end{cpp}

使用llvm::dyn\_cast<TypeDeclaration>检查类型声明。若不是类型声明,则打印错误消息;否则,对于Ids列表中的每个名称,将实例化VariableDeclaration并添加到声明列表中。若将变量添加到当前作用域中失败,因为变量名已经声明,也会输出一条错误消息。

大多数其他实体以相同的方式构建——语义分析的复杂性是唯一的区别。模块和过程需要做更多的工作,打开了一个新的作用域。打开一个新的作用域很容易:只需要实例化一个新的作用域对象。在解析模块或过程之后,必须删除作用域。

这一类实现必须可靠,我们不想在出现语法错误时将名称添加到错误的作用域中,这是C++中资源获取即初始化(Resource Acquisition Is Initialization, RAII)习惯用法的经典用法。另一个复杂之处在于过程可以递归地调用自身,所以在使用过程之前,必须将其名称添加到当前作用域。语义分析器有两个方法来进入和离开作用域。作用域与声明相关联:

\begin{cpp}
void Sema::enterScope(Decl *D) {
    CurrentScope = new Scope(CurrentScope);
    CurrentDecl = D;
}

void Sema::leaveScope() {
    Scope *Parent = CurrentScope->getParent();
    delete CurrentScope;
    CurrentScope = Parent;
    CurrentDecl = CurrentDecl->getEnclosingDecl();
}
\end{cpp}

一个简单的辅助类用于实现RAII惯用法:

\begin{cpp}
class EnterDeclScope {
    Sema &Semantics;

public:
    EnterDeclScope(Sema &Semantics, Decl *D)
    : Semantics(Semantics) {
        Semantics.enterScope(D);
    }
    ~EnterDeclScope() { Semantics.leaveScope(); }
};
\end{cpp}

解析模块或过程时,语义分析器会进行两次交互。第一个是在名字被解析之后,构造了(几乎是空的)AST节点,并建立了一个新的作用域:

\begin{cpp}
bool Parser::parseProcedureDeclaration(/* … */) {
    /* … */
    if (consume(tok::kw_PROCEDURE)) return _errorhandler();
    if (expect(tok::identifier)) return _errorhandler();
    ProcedureDeclaration *D =
        Actions.actOnProcedureDeclaration(
            Tok.getLocation(), Tok.getIdentifier());
    EnterDeclScope S(Actions, D);
    /* … */
}
\end{cpp}

语义分析器检查当前范围中的名称,返回AST节点:

\begin{cpp}
ProcedureDeclaration *
Sema::actOnProcedureDeclaration(SMLoc Loc, StringRef Name) {
    ProcedureDeclaration *P =
    new ProcedureDeclaration(CurrentDecl, Loc, Name);
    if (!CurrentScope->insert(P))
        Diags.report(Loc, diag::err_symbold_declared, Name);
    return P;
}
\end{cpp}

真正的工作是在所有声明和过程解析之后完成的。只需要检查过程声明末尾的名称是否与过程名称相等,以及用于返回类型的声明是否为类型声明:

\begin{cpp}
void Sema::actOnProcedureDeclaration(
        ProcedureDeclaration *ProcDecl, SMLoc Loc,
        StringRef Name, FormalParamList &Params, Decl *RetType,
        DeclList &Decls, StmtList &Stmts) {

    if (Name != ProcDecl->getName()) {
        Diags.report(Loc, diag::err_proc_identifier_not_equal);
        Diags.report(ProcDecl->getLocation(),
                     diag::note_proc_identifier_declaration);
    }
    ProcDecl->setDecls(Decls);
    ProcDecl->setStmts(Stmts);

    auto *RetTypeDecl =
    dyn_cast_or_null<TypeDeclaration>(RetType);
    if (!RetTypeDecl && RetType)
        Diags.report(Loc, diag::err_returntype_must_be_type, Name);
    else
        ProcDecl->setRetType(RetTypeDecl);
}
\end{cpp}

有些声明本身就存在,不能由开发人员定义。这包括BOOLEAN和INTEGER类型以及TRUE和FALSE字面值,这些声明存在于全局作用域中,必须以编程方式添加。Modula-2还预定义了一些程序,如INC或DEC,可以添加到全局作用域。对于我们的类,初始化全局作用域很简单:

\begin{cpp}
void Sema::initialize() {
    CurrentScope = new Scope();
    CurrentDecl = nullptr;
    IntegerType =
        new TypeDeclaration(CurrentDecl, SMLoc(), "INTEGER");
    BooleanType =
        new TypeDeclaration(CurrentDecl, SMLoc(), "BOOLEAN");
    TrueLiteral = new BooleanLiteral(true, BooleanType);
    FalseLiteral = new BooleanLiteral(false, BooleanType);
    TrueConst = new ConstantDeclaration(CurrentDecl, SMLoc(),
                                        "TRUE", TrueLiteral);
    FalseConst = new ConstantDeclaration(
        CurrentDecl, SMLoc(), "FALSE", FalseLiteral);
    CurrentScope->insert(IntegerType);
    CurrentScope->insert(BooleanType);
    CurrentScope->insert(TrueConst);
    CurrentScope->insert(FalseConst);
}
\end{cpp}

使用该方案,tinylang所需的所有计算都可以完成。例如,下面来看看如何计算表达式的结果是否为常量:

\begin{itemize}
\item
必须确保常量声明的字面量或引用是常量

\item
表达式的两边都是常量,运算符也会得到一个常量
\end{itemize}

为表达式创建AST节点时,这些规则被嵌入到语义分析器中,也可以计算类型和常数值。

但并不是所有种类的计算都可以用这种方法进行。例如,要检测未初始化变量的使用,可以使用一种称为符号解释的方法。该方法需要一个特殊的遍历AST的顺序,在构造期间不可能获取。好消息是,所提出的方法创建了一个完全修饰过的AST,可以为代码生成做好准备。这个AST可以用于进一步的分析,所以昂贵的分析可以根据需要打开或关闭。

要使用前端,还需要更新驱动程序。由于缺少代码生成,正确的tinylang程序不会产生任何输出。尽管如此,其仍可以用于探索错误恢复和引发语义错误:

\begin{cpp}
#include "tinylang/Basic/Diagnostic.h"
#include "tinylang/Basic/Version.h"
#include "tinylang/Parser/Parser.h"
#include "llvm/Support/InitLLVM.h"
#include "llvm/Support/raw_ostream.h"

using namespace tinylang;

int main(int argc_, const char **argv_) {
    llvm::InitLLVM X(argc_, argv_);

    llvm::SmallVector<const char *, 256> argv(argv_ + 1,
                                              argv_ + argc_);
    llvm::outs() << "Tinylang "
                 << tinylang::getTinylangVersion() << "\n";

    for (const char *F : argv) {
        llvm::ErrorOr<std::unique_ptr<llvm::MemoryBuffer>>
            FileOrErr = llvm::MemoryBuffer::getFile(F);
        if (std::error_code BufferError =
                FileOrErr.getError()) {
            llvm::errs() << "Error reading " << F << ": "
                         << BufferError.message() << "\n";
            continue;
        }

        llvm::SourceMgr SrcMgr;
        DiagnosticsEngine Diags(SrcMgr);
        SrcMgr.AddNewSourceBuffer(std::move(*FileOrErr),
                                  llvm::SMLoc());
        auto TheLexer = Lexer(SrcMgr, Diags);
        auto TheSema = Sema(Diags);
        auto TheParser = Parser(TheLexer, TheSema);
        TheParser.parse();
    }
}
\end{cpp}

恭喜!已经完成了tinylang的前端实现!可以使用示例程序Gcd.mod(在定义真正的编程语言一节中提供),以运行前端:

\begin{shell}
$ tinylang Gcd.mod
\end{shell}

这是一个有效的程序,看起来好像什么也没发生。一定要尝试修改代码文件,使它输出一些错误消息。我们将在下一章中添加代码生成的过程。


================================================
FILE: content/part2/chapter3/8.tex
================================================
本章中,了解了实际编译器在前端使用的技术。从项目布局开始,为词法分析器、解析器和语义分析器创建了单独的库。为了向用户输出消息,扩展了一个现有的LLVM类,允许集中存储消息,词法分析器现在已经分成几个接口。

然后,了解了如何根据语法描述构造递归下降解析器,了解了要避免哪些陷阱,如何使用生成器来完成这项工作。构建的语义分析器执行语言所需的所有语义检查,同时与解析器和AST构造交织在一起。

编码工作的结果是一个完全修饰的AST,你将在下一章中使用它来生成IR代码,最后生成汇编代码。


================================================
FILE: content/part2/chapter4/0.tex
================================================

为编程语言创建了修饰抽象语法树(AST)之后,下一个任务是从中生成LLVM IR代码。LLVM IR代码类似于具有人类可读表示的三地址码,所以需要一种系统的方法来将语言概念(如控制结构)翻译成低层LLVM IR。

本章中,将了解LLVM IR的基础知识,如何从AST为控制流结构生成IR,如何使用现代算法为静态单赋值(SSA)形式的表达式生成LLVM IR,如何生成汇编文本和目标代码。

本章中,将了解以下内容:

\begin{itemize}
\item
使用AST生成IR

\item
使用AST编号生成SSA格式的IR代码

\item
设置模块和驱动程序
\end{itemize}

本章结束时,将了解如何为编程语言创建代码生成器,以及如何将其集成到编译器中。



================================================
FILE: content/part2/chapter4/1.tex
================================================

LLVM代码生成器将LLVM IR中的一个模块作为输入,并将其转换为目标代码或汇编文本,需要将AST表示转换为IR。为了实现一个IR代码生成器,首先看一个简单的例子,然后开发代码生成器所需的类。完整的实现将分为三类:

\begin{itemize}
\item
CodeGenerator

\item
CGModule

\item
CGProcedure
\end{itemize}

CodeGenerator类是编译器驱动程序使用的通用接口,CGModule和CGProcedure类保存为编译单元和单个函数生成IR代码所需的状态。

我们将从clang生成的IR开始。

\mySubsubsection{4.1.1.}{理解IR代码}

生成IR代码之前,最好先了解一下IR语言的主要元素。在第2章中,简要介绍了IR。获得更多IR知识的一个简单方法是研究clang的输出。例如,这个C代码(gcd.c),其实现了计算两个数的最大公约数的欧几里得算法:

\begin{cpp}
unsigned gcd(unsigned a, unsigned b) {
    if (b == 0)
    return a;
    while (b != 0) {
        unsigned t = a % b;
        a = b;
        b = t;
    }
    return a;
}
\end{cpp}

使用clang创建IR文件(gcd.ll):

\begin{shell}
$ clang --target=aarch64-linux-gnu -O1 -S -emit-llvm gcd.c
\end{shell}

IR代码与目标有关,说明该命令用于编译Linux下ARM 64位CPU的源文件。-S选项指示clang输出一个程序集文件,通过设置-emit-llvm选项,创建一个IR文件。优化级别-O1,用于获得一个易于阅读的IR代码。Clang有更多的选项,所有这些选项都在\url{https://clang.llvm.org/docs/ClangCommandLineReference.html}的命令行参数引用中进行了记录。来看一下生成的文件,并理解C代码是如何映射到LLVM IR的。

一个C文件翻译成一个模块,其中包含函数和数据对象。一个函数至少有一个基本块,一个基本块包含指令,这种分层结构也反映在C++ API中。所有数据元素都有类型,整数类型由字母i和位数表示。例如,64位整数类型写为i64。最基本的浮点类型是float和double,分别表示32位和64位IEEE浮点类型。也可以创建聚合类型,如vector、数组和结构体。

以下是LLVM IR,在文件的顶部,确定了一些基本属性。

\begin{shell}
; ModuleID = 'gcd.c'
source_filename = "gcd.c"
target datalayout = "e-m:e-i8:8:32-i16:16:32-i64:64-i128:128-
n32:64-S128"
target triple = "aarch64-unknown-linux-gnu"
\end{shell}

第一行是一个注释,表面使用了哪个模块标识符。下一行中,将命名源文件的文件名。对于clang,两者一样。

目标数据布局字符串建立一些基本属性。不同的部分用-号隔开。包括以下信息:

\begin{itemize}
\item
小写e表示内存中的字节使用小端模式存储。要指定大端序,必须使用大写E。

\item
M: 指定应用于符号的名称改写。这里,m:e表示使用ELF名称改写。

\item
iN:A:P形式,例如i8:8:32,指定了数据的对齐方式,以位表示。第一个数字是ABI要求的对齐方式,第二个数字是首选对齐方式。对于字节(i8), ABI对齐是1字节(8),首选对齐是4字节(32)。

\item
n指定可用的本机寄存器大小,n32:64表示原生支持32位和64位宽的整数。

\item
S指定了栈的对齐方式,同样以位为单位。S128表示栈保持16字节对齐。
\end{itemize}

\begin{myNotic}{Note}
所提供的目标数据布局必须与后端期望的匹配,将捕获的信息传递给与目标无关的优化过程。例如,优化过程可以查询数据布局以获得指针的大小和对齐方式,但改变数据布局中指针的大小,并不会改变后端代码的生成。

目标数据布局提供了更多的信息,可以在\url{https://llvm.org/docs/LangRef.html#data-layout}的参考手册中找到更多信息。
\end{myNotic}

最后,目标三重字符串指定了我们要编译的架构,在命令行上给出的信息。三元组是一个配置字符串,通常由CPU架构、厂商和操作系统组成,通常还会添加更多关于环境的信息。例如,x86\_64-pc-win32三元组用于运行在64位x86 CPU上的Windows系统。x86\_64是CPU架构,pc是通用的供应商,win32是操作系统。各部分用连字符连接。在ARMv8 CPU上运行的Linux系统使用aarch64-unknown-linux-gnu作为它的三元组。aarch64是CPU架构,而操作系统是运行gnu环境的linux。基于linux的系统没有真正的供应商,所以这部分未知。对于特定目的而言,不知道或不重要的部分通常忽略:所以aarch64-linux-gnu和aarch64-unknown-linux-gnu三元组描述了同一个Linux系统。

接下来,在IR文件中定义gcd函数:

\begin{shell}
define i32 @gcd(i32 %a, i32 %b) {
\end{shell}

这类似于C文件中的函数签名。无符号数据类型被转换成32位整数类型i32。函数名以@为前缀,参数名以\%为前缀。函数体用大括号括起来:

\begin{shell}
entry:
    %cmp = icmp eq i32 %b, 0
    br i1 %cmp, label %return, label %while.body
\end{shell}

IR代码组织成基本块,格式良好的基本块是一个线性指令序列,以一个可选的标签开始,以一个终止指令结束,所以每个基本块都有一个入口点和一个出口点。LLVM允许在构造时使用畸形基本块,第一个基本块的标签是entry。代码块中的代码很简单:第一条指令将\%b参数与0进行比较。第二条指令在条件为true时跳转到return标签,然后跳转到while语句。若条件为false,则使用body标签。

IR代码的另一个特点是采用静态单一赋值(SSA)形式。代码使用无限量的虚拟寄存器,但每个寄存器只写入一次。比较的结果会赋值给指定的虚拟寄存器\%cmp,使用这个寄存器,但不再写入。诸如常量传播和公共子表达式消除之类的优化,在SSA形式下工作得非常好,所有现代编译器都在使用。

\begin{myTip}{SSA}
SSA是在20世纪80年代后期发展起来的,因其简化了数据流分析和优化,所以广泛应用于编译器中。例如,R是SSA形式,循环内部公共子表达式的识别就会容易得多。SSA的一个基本属性是建立了def-use和use-def链:对于单个定义,可知道所有的用法(def-use),对于每个用法,知道唯一的定义(use-def)。这个信息广泛使用,例如在常量传播中:若一个定义确定为常量,则所有对该值的使用都可以很容易地替换为该常量值。

Cytron等人(1989)提出的构造SSA形式的算法非常流行,也用于LLVM的实现。早期的观察发现,若语言没有goto语句,这些算法会变得更简单。

对SSA的深入研究可以在F. rastello和F. B. Tichadou的《基于SSA的编译器设计》一书中找到,Springer 2022。
\end{myTip}

下一个基本块是while循环的主体:

\begin{shell}
while.body:
    %b.loop = phi i32 [ %rem, %while.body ],
                        [ %b, %entry ]
    %a.loop = phi i32 [ %b.loop, %while.body ],
                        [ %a, %entry ]
    %rem = urem i32 %a.loop, %b.loop
    %cmp1 = icmp eq i32 %rem, 0
    br i1 %cmp1, label %return, label %while.body
\end{shell}

在gcd的循环中,参数a和b会赋新值。若一个寄存器只能写入一次,则是不可能的。解决方案是使用特殊的phi指令,phi指令有一个基本块和值的列表作为参数。基本块表示来自该基本块的传入边,值是来自该基本块的值。运行时,phi指令将之前执行的基本块的标签与参数列表中的标签进行比较。

指令的值就是与标签相关联的值。对于第一条phi指令,若先前执行的基本块是while.body,则该值为\%rem寄存器。若先前执行的基本块,则值为\%b,这些值是位于基本块开头的值。\%b循环寄存器从第一个phi指令获取一个值。第二个phi指令的参数列表中使用了相同的寄存器,但假定该值是通过第一个phi指令改变之前的值。

循环体之后,必须选择返回值:

\begin{shell}
return:
    %retval = phi i32 [ %a, %entry ],
                      [ %b.loop, %while.body ]
    ret i32 %retval
}
\end{shell}

同样,phi指令用于选择所需的值。ret指令不仅结束了这个基本块,运行时表示这个函数的结束。它有一个返回值作为参数。

使用phi指令有一些限制,必须是基本块的第一个指令。第一个基本块比较特殊:没有之前执行过的块,所以不能以phi指令开始。

\begin{myTip}{LLVM IR参考}
我们只触及了LLVM IR的皮毛,访问LLVM语言参考手册\url{https://llvm.org/docs/LangRef.html},可了解更多细节。
\end{myTip}

IR代码本身看起来很像C语言和汇编语言的混合体。尽管有这种熟悉的风格,但还是不清楚如何使用AST生成IR代码,尤其是phi指令看起来很难生成。下一节中,将为此实现一个算法!

\mySubsubsection{4.1.2.}{了解加载和存储的方法}

LLVM中的所有局部优化都是基于这里所示的SSA,使用全局变量的内存引用。IR语言有load和store指令,用于获取和存储这些值,也可以将此用于局部变量。这些指令不属于SSA形式,LLVM知道如何将其转换为所需的SSA,可以为每个局部变量分配内存槽,并使用load和store指令来更改它们的值,只需要记住指向存储变量的内存槽的指针。clang编译器使用这种方法。

来看一下load和store的IR代码。再次编译gcd.c,但这次没有启用优化:

\begin{shell}
$ clang --target=aarch64-linux-gnu -S -emit-llvm gcd.c
\end{shell}

gcd函数现在看起来有所不同。这是第一个基本块:

\begin{shell}
define i32 @gcd(i32, i32) {
    %3 = alloca i32, align 4
    %4 = alloca i32, align 4
    %5 = alloca i32, align 4
    %6 = alloca i32, align 4
    store i32 %0, ptr %4, align 4
    store i32 %1, ptr %5, align 4
    %7 = load i32, ptr %5, align 4
    %8 = icmp eq i32 %7, 0
    br i1 %8, label %9, label %11
\end{shell}

IR代码现在依赖于寄存器和标签的自动编号,未指定参数的名称,隐式地为\%0和\%1。基本块没有标签,因此赋值为2,前几条指令为4个32位值分配内存。之后,参数\%0和\%1存储在寄存器\%4和\%5指向的内存槽中。为了比较\%1和0,这个值显式地从内存槽加载。使用这种方法,不需要使用phi指令!相反,可以从内存槽中加载一个值进行计算,然后将新值存储回内存槽中。下次读取内存槽时,就会得到最后一次计算的值。gcd函数的所有其他基本块都遵循这种模式。

以这种方式使用加载和存储指令的优点是,生成IR代码相当容易;缺点是,在将基本块转换为SSA形式后,在第一个优化步骤中,LLVM将使用mem2reg时,会删除大量的IR指令。

因此,我们直接生成SSA形式的IR代码,通过将控制流映射到基本块开始生成IR代码。

\mySubsubsection{4.1.3.}{控制流映射到基本块}

基本块的概念,是按该顺序执行的指令的线性序列。基本块在开始时只有一个条目,以终止指令结束,终止指令是将控制流转移到另一个基本块的指令,例如:分支指令、切换指令或返回指令。请参阅\url{https://llvm.org/docs/LangRef.html#terminator-instructions}获取终止器说明的完整列表。一个基本块可以以phi指令开始,但在一个基本块内,既不允许phi指令,也不允许分支指令。换句话说,只能在第一个指令中输入一个基本块,并且只能在最后一个指令中留下一个基本块,即结束指令。不可能在基本块内分支到指令,也不可能从基本块中间分支到另一个基本块。请注意,带有call指令的简单函数调用可以发生在基本块中。每个基本块只有一个标签,标记基本块的第一条指令,标签是分支指令的目标。可以将分支视为两个基本块之间的有向边,从而生成控制流图(CFG)。一个基本块可以有前身和后继,函数的第一个基本块是特殊的(因为它不允许有前块)。

由于这些限制,源语言中的控制语句,如WHILE和IF,会产生几个基本块。来看一下WHILE语句。WHILE语句的条件控制是执行循环体还是执行下一个语句,该条件语句必须在一个单独的基本块中生成,因为其两个前块:

\begin{itemize}
\item
由WHILE之前的语句产生的基本块

\item
从循环体的末端返回到条件的分支
\end{itemize}

还有两个后块:

\begin{itemize}
\item
循环体的开始部分

\item
由WHILE后面的语句产生的基本块
\end{itemize}

循环体本身至少有一个基本块:

\myGraphic{0.5}{content/part2/chapter4/images/1.png}{图4.1 - WHILE语句的基本块}

IR代码生成遵循这种结构。在CGProcedure类中存储一个指向当前基本块的指针,并使用llvm::IRBuilder<>的实例将指令插入基本块。创建基本块:

\begin{cpp}
void emitStmt(WhileStatement *Stmt) {
    llvm::BasicBlock *WhileCondBB = llvm::BasicBlock::Create(
        CGM.getLLVMCtx(), "while.cond", Fn);
    llvm::BasicBlock *WhileBodyBB = llvm::BasicBlock::Create(
        CGM.getLLVMCtx(), "while.body", Fn);
    llvm::BasicBlock *AfterWhileBB = llvm::BasicBlock::Create(
        CGM.getLLVMCtx(), "after.while", Fn);
\end{cpp}

Fn变量表示当前函数,getLLVMCtx()返回LLVM上下文,两者都会在稍后设定。我们用一个基本块的分支来结束当前的基本块,该分支将保存以下条件:

\begin{cpp}
    Builder.CreateBr(WhileCondBB);
\end{cpp}

该条件的基本块成为新的当前基本块。生成条件语句,并以一个条件分支结束代码块:

\begin{cpp}
    setCurr(WhileCondBB);
    llvm::Value *Cond = emitExpr(Stmt->getCond());
    Builder.CreateCondBr(Cond, WhileBodyBB, AfterWhileBB);
\end{cpp}

接下来,生成循环体,在条件语句的基本块中添加一个分支:

\begin{cpp}
    setCurr(WhileBodyBB);
    emit(Stmt->getWhileStmts());
    Builder.CreateBr(WhileCondBB);
\end{cpp}

这样,就生成了WHILE语句。现在已经生成了WhileCondBB和Curr块,可以对其进行密封:

\begin{cpp}
    sealBlock(WhileCondBB);
    sealBlock(Curr);
\end{cpp}

WHILE语句后面的空基本块变成了当前基本块:

\begin{cpp}
    setCurr(AfterWhileBB);
}
\end{cpp}

按照这个模式,可以为语言的每个语句创建emit()方法。



































================================================
FILE: content/part2/chapter4/2.tex
================================================

为了使用AST生成SSA形式的IR代码,可以使用一种称为AST编号的方法。基本思想是,对于每个基本块,我们存储写入该基本块中局部变量的当前值。

\begin{myNotic}{Note}
实现基于Braun et al.的论文《Simple and Efficient Construction of Static Single Assignment Form》,International Conference on CompilerConstruction 2013 (CC 2013), Springer(见\url{http://individual.utoronto.ca/dfr/ece467/braun13.pdf})。在其呈现的形式中,仅适用于具有结构化受控流的IR代码。若需要支持任意控制流,本文还描述了必要的扩展——例如:goto。
\end{myNotic}

虽然很简单,但仍然需要几个步骤。首先介绍所需的数据结构,然后了解如何在基本块的局部读写值。然后,将处理几个基本块中使用的值,并通过优化创建的phi指令来结束。

\mySubsubsection{4.2.1.}{定义数据结构保存值}

使用BasicBlockDef struct来保存单个块的信息:

\begin{cpp}
struct BasicBlockDef {
    llvm::DenseMap<Decl *, llvm::TrackingVH<llvm::Value>> Defs;
    // ...
};
\end{cpp}

llvm::Value类表示SSA格式的值,其作用类似于计算结果的标签。通过IR指令创建,然后使用。优化过程中可能会发生各种变化,若优化器检测到\%1和\%2的值始终相同,可将\%2替换为\%1。这会改变标签,但不会改变计算。

为了了解这些变化,不能直接使用Value类。相反,这时需要一个值句柄。Value的处理有不同的功能。为了跟踪替换,需要使用llvm::TrackingVH<>类,所以Defs成员将AST的声明(变量或正式参数)映射到其当前值,需要为每个基本块存储这些信息:

\begin{cpp}
llvm::DenseMap<llvm::BasicBlock *, BasicBlockDef> CurrentDef;
\end{cpp}

有了这个数据结构,就可以处理局部值了。

\mySubsubsection{4.2.2.}{定义保存值的数据结构}

为了在基本块中存储局部变量的当前值,将在map中创建一个条目:

\begin{cpp}
void writeLocalVariable(llvm::BasicBlock *BB, Decl *Decl,
                        llvm::Value *Val) {
    CurrentDef[BB].Defs[Decl] = Val;
}
\end{cpp}

因为值可能不在基本块中,所以变量值的查找有点复杂,所以需要使用可能的递归搜索将搜索扩展到前面的节点:

\begin{cpp}
llvm::Value *
readLocalVariable(llvm::BasicBlock *BB, Decl *Decl) {
    auto Val = CurrentDef[BB].Defs.find(Decl);
    if (Val != CurrentDef[BB].Defs.end())
        return Val->second;
    return readLocalVariableRecursive(BB, Decl);
}
\end{cpp}

实际的工作是搜索前面的块,将在下一节中实现。

\mySubsubsection{4.2.3.}{前块中搜索值}

若正在查看的当前基本块只有一个前块,则直接在那里搜索变量值。若基本块有多个前块,需要在所有这些块中搜索值并组合结果。为了说明这种情况,可以看看前一节中使用WHILE条件语句的基本块。

这个基本块有两个前块——一个来自WHILE语句之前的语句,另一个来自WHILE循环主体末尾的分支。在条件中使用的变量应该有一些初始值,并且最有可能在循环体中更改,所以需要收集这些定义并创建phi指令。从WHILE语句创建一个包含循环的基本块。

因为需要递归地搜索前块,所以必须打破这种循环。可以使用一个简单的技巧:插入一个空的phi指令,并将其记录为变量的当前值。若在搜索中再次看到这个基本块,则看到该变量有一个可以使用的值,搜索到这里就停止了。当收集了所有的值,就必须更新phi指令。

然而,我们仍然会面临一个问题。在查找的时候,并不是一个基本块的所有前块都已知。来看看为WHILE语句创建的基本块,首先生成循环条件的IR,但只有在主体的IR生成之后,才能添加从主体末尾返回到包含条件基本块的分支(这是因为这个基本块之前是未知的)。若需要在条件中读取变量的值,那就卡住了,因为不是所有的前块都已知。

为了解决这个问题,必须做点什么:

\begin{enumerate}
\item
首先,必须在基本块上添加一个密封(Sealed)标志。

\item
然后,若知道基本块的所有前块,必须将基本块定义为密封。若基本块没有密封,并且需要查找这个基本块中尚未定义变量的值,必须插入一个空的phi指令,并将其作为值。

\item
也需要记住这个指令。若块稍后密封,则需要用实际值更新指令。为了实现这一点,必须在结构体BasicBlockDef中添加两个成员:IncompletePhis映射,记录了稍后需要更新的phi指令,以及Sealed标志,这表示基本块是否密封:

\begin{cpp}
llvm::DenseMap<llvm::PHINode *, Decl *> IncompletePhis;
unsigned Sealed : 1;
\end{cpp}

\item
该方法的实现,如本节开头所讨论:

\begin{cpp}
llvm::Value *CGProcedure::readLocalVariableRecursive(
        llvm::BasicBlock *BB, Decl *Decl) {
    llvm::Value *Val = nullptr;
    if (!CurrentDef[BB].Sealed) {
        llvm::PHINode *Phi = addEmptyPhi(BB, Decl);
        CurrentDef[BB].IncompletePhis[Phi] = Decl;
        Val = Phi;
    } else if (auto *PredBB = BB->getSinglePredecessor()) {
        Val = readLocalVariable(PredBB, Decl);
    } else {
        llvm::PHINode *Phi = addEmptyPhi(BB, Decl);
        writeLocalVariable(BB, Decl, Phi);
        Val = addPhiOperands(BB, Decl, Phi);
    }
    writeLocalVariable(BB, Decl, Val);
    return Val;
}
\end{cpp}

\item
addEmptyPhi()方法在基本块的开头插入一个空的phi指令:

\begin{cpp}
llvm::PHINode *
CGProcedure::addEmptyPhi(llvm::BasicBlock *BB,
        Decl *Decl) {
    return BB->empty()
        ? llvm::PHINode::Create(mapType(Decl), 0,
                                "", BB)
        : llvm::PHINode::Create(mapType(Decl), 0,
                                "", &BB->front());
}
\end{cpp}

\item
要将缺失的操作数添加到phi指令中,必须首先搜索基本块的所有前块,并将操作数值对和基本块添加到phi指令中,需要尝试优化指令:

\begin{cpp}
llvm::Value *
CGProcedure::addPhiOperands(llvm::BasicBlock *BB,
                            Decl *Decl,
                            llvm::PHINode *Phi) {
    for (auto *PredBB : llvm::predecessors(BB))
        Phi->addIncoming(readLocalVariable(PredBB, Decl),
                         PredBB);
    return optimizePhi(Phi);
}
\end{cpp}

\end{enumerate}

该算法可以产生不必要的phi指令,有种方法可以用来进行优化,这将在下一节中实现。

\mySubsubsection{4.2.4.}{优化生成的phi指令}

如何优化指令,为什么要这样做?尽管SSA形式对许多优化都有利,但算法通常不能解析phi指令,从而阻碍了优化,所以生成的phi指令越少越好:

\begin{enumerate}
\item
若指令只有一个操作数,或者所有操作数的值都相同,则用这个值替换指令。若该指令没有操作数,则用特殊的Undef值替换该指令。只有当指令有两个或更多不同的操作数时,才必须保留这条指令:

\begin{cpp}
llvm::Value *
CGProcedure::optimizePhi(llvm::PHINode *Phi) {
    llvm::Value *Same = nullptr;
    for (llvm::Value *V : Phi->incoming_values()) {
        if (V == Same || V == Phi)
            continue;
        if (Same && V != Same)
            return Phi;
        Same = V;
    }
    if (Same == nullptr)
        Same = llvm::UndefValue::get(Phi->getType());
\end{cpp}


\item
删除一条phi指令可能会给其他phi指令带来优化机会,LLVM会跟踪用户和值的使用(即SSA定义中提到的use-def链),必须在其他phi指令中搜索该值使用情况,并尝试优化这些指令:

\begin{cpp}
    llvm::SmallVector<llvm::PHINode *, 8> CandidatePhis;
    for (llvm::Use &U : Phi->uses()) {
        if (auto *P =
                llvm::dyn_cast<llvm::PHINode>(U.getUser()))
        if (P != Phi)
            CandidatePhis.push_back(P);
    }
    Phi->replaceAllUsesWith(Same);
    Phi->eraseFromParent();
    for (auto *P : CandidatePhis)
        optimizePhi(P);
    return Same;
}
\end{cpp}

\end{enumerate}

还可以进一步改进这个算法,可以选择并记住两个不同的值,而不是总是迭代每个phi指令的值列表。在optimizePhi函数中,可以检查这两个值是否仍然在phi指令的列表中,则没必要优化。但即使没有这个优化,这个算法也可以运行得也很快,所以现在不进行实现。

现在,唯一没有做的是实现密封基本块的操作。

\mySubsubsection{4.2.5.}{密封块}

了解了一个区块的所有前块,就可以密封这个块了。若语言只包含结构化语句(如tinylang),那么很容易确定块的位置。再看一下为WHILE语句生成的基本块。

包含条件的基本块可以在主体的末端添加分支后进行密封,这是缺少的最后一个前块。要密封一个块,可以简单地将缺失的操作数,添加到不完整的phi指令中并设置标志:

\begin{cpp}
void CGProcedure::sealBlock(llvm::BasicBlock *BB) {
    for (auto PhiDecl : CurrentDef[BB].IncompletePhis) {
        addPhiOperands(BB, PhiDecl.second, PhiDecl.first);
    }
    CurrentDef[BB].IncompletePhis.clear();
    CurrentDef[BB].Sealed = true;
}
\end{cpp}

有了这些方法,就可以为表达式生成IR了。

\mySubsubsection{4.2.6.}{生成表达式的IR}

要翻译表达式,如第2章所示。唯一有趣的部分是如何访问变量。上一节讨论了局部变量,但还要考虑其他类型的变量。这里,就讨论一下接下来需要些做什么:

\begin{itemize}
\item
对于过程的局部变量,使用上一节中的readLocalVariable()和writeLocalVariable()方法。

\item
对于一个封闭过程中的局部变量,需要一个指向封闭过程框架的指针。这将在本章稍后的章节中进行处理。

\item
对于全局变量,生成加载和存储指令。

\item
对于形式参数,必须区分按值传递和按引用传递(tinylang中的VAR参数)。通过值传递的参数会视为局部变量,通过引用传递的参数会当作全局变量。
\end{itemize}

综上所述,得到如下用于读取变量或形参的代码:

\begin{cpp}
llvm::Value *CGProcedure::readVariable(llvm::BasicBlock *BB,
Decl *D) {
    if (auto *V = llvm::dyn_cast<VariableDeclaration>(D)) {
        if (V->getEnclosingDecl() == Proc)
            return readLocalVariable(BB, D);
        else if (V->getEnclosingDecl() == CGM.getModuleDeclaration()) {
            return Builder.CreateLoad(mapType(D),
            CGM.getGlobal(D));
        } else
        llvm::report_fatal_error("Nested procedures not yet supported");
    } else if (auto *FP = llvm::dyn_cast<FormalParameterDeclaration>(D)) {
        if (FP->isVar()) {
            return Builder.CreateLoad(mapType(FP, false),
            FormalParams[FP]);
        } else
        return readLocalVariable(BB, D);
    } else
        llvm::report_fatal_error("Unsupported declaration");
}
\end{cpp}

对变量或形式参数的写入是对称的——需要将要读的方法与要写的方法交换,并使用存储指令,而非加载指令。

接下来,在为这些函数生成IR代码时,应用这些函数。

\mySubsubsection{4.2.7.}{生成函数的IR}

大多数IR代码都存在于函数中,IR代码中的函数类似于C语言中的函数,通过名称、参数类型、返回值和其他属性指定。要调用不同编译单元中的函数,需要声明该函数,这与C语言中的原型类似。若向函数中添加基本块,就可以定义函数。接下来的几节会介绍这些内容,但首先要讨论的是,符号名称的可见性。


\mySubsubsection{4.2.8.}{通过链接和名称修改控制可见性}

函数(以及全局变量)具有附加的链接样式。使用链接方式,我们定义了符号名称的可见性,若多个符号具有相同的名称会发生什么。最基本的链接样式是私有的和外部的,私有链接的符号仅在当前编译单元中可见,而具有外部链接的符号则全局可见。

对于没有模块概念的语言(如C语言),这是足够的。对于模块,需要做更多的事情。假设有一个模块Square,提供了一个Root()函数,还有一个模块Cube,也提供了一个Root()函数。若函数私有,那么没什么问题,该函数获取名称Root和私有链接。若导出函数,以便可以从其他模块调用它,情况就不同了。因为函数名不唯一,所以仅使用函数名就不够了。

解决方案是调整名称使其全局唯一,这称为名称修改,如何做到取决于语言的要求和特点。在我们的示例中,基本思想是使用模块名和函数名的组合来创建全局唯一的名称。使用Square.Root作为名称看起来是一个显而易见的解决方案,但这可能会导致汇编程序的问题,因为点可能具有特殊的含义。不需要在名称组件之间使用分隔符,可以通过在名称组件前加上长度:6Square4Root来获得类似的效果。单这不是LLVM的合法标识符,我们可以通过在整个名称前加上\_t (t表示tinylang)来修复这个问题:\_t6Square4Root。通过这种方式,可以为导出符号创建唯一名称:

\begin{cpp}
std::string CGModule::mangleName(Decl *D) {
    std::string Mangled("_t");
    llvm::SmallVector<llvm::StringRef, 4> List;
    for (; D; D = D->getEnclosingDecl())
        List.push_back(D->getName());
    while (!List.empty()) {
        llvm::StringRef Name = List.pop_back_val();
        Mangled.append(llvm::Twine(Name.size()).concat(Name).str());
    }
    return Mangled;
}
\end{cpp}

若语言支持类型重载,则需要使用类型名扩展此方案。例如,C++中中要区分int root(int)和double root(double)函数,必须将形参类型和返回值添加到函数名中。还需要考虑生成的名称的长度,因为某些链接器对长度进行了限制。在C++中使用嵌套的命名空间和类,修改后的名称可能相当长。

这里,C++定义了一个压缩方案,以避免一遍又一遍地重复名称组件。

接下来,就来看看如何处理函数参数。

\mySubsubsection{4.2.9.}{AST描述类型转换为LLVM类型}

函数的参数也需要考虑。首先,需要将源语言的类型映射到LLVM类型。由于tinylang目前只有两种类型,所以很容易:

\begin{cpp}
llvm::Type *CGModule::convertType(TypeDeclaration *Ty) {
    if (Ty->getName() == "INTEGER")
        return Int64Ty;
    if (Ty->getName() == "BOOLEAN")
        return Int1Ty;
    llvm::report_fatal_error("Unsupported type");
}
\end{cpp}

Int64Ty、Int1Ty和VoidTy是类成员,包含i64、i1和void LLVM类型的类型表示。

对于通过引用传递的形参,这是不够的。该参数的LLVM类型为指针型,当我们想要使用形参的值时,需要知道底层类型。这由HonorReference标志控制,其默认值为true。我们对函数进行推广,并考虑形参:

\begin{cpp}
llvm::Type *CGProcedure::mapType(Decl *Decl,
                                 bool HonorReference) {
    if (auto *FP = llvm::dyn_cast<FormalParameterDeclaration>(
    Decl)) {
        if (FP->isVar() && HonorReference)
        return llvm::PointerType::get(CGM.getLLVMCtx(),
        /*AddressSpace=*/0);
        return CGM.convertType(FP->getType());
    }
    if (auto *V = llvm::dyn_cast<VariableDeclaration>(Decl))
        return CGM.convertType(V->getType());
    return CGM.convertType(llvm::cast<TypeDeclaration>(Decl));
}
\end{cpp}

有了这些辅助程序,我们就可以创建LLVM IR函数了。

\mySubsubsection{4.2.10.}{创建LLVM IR函数}

要在LLVM IR中生成一个函数,需要一个函数类型,这类似于C中的原型。创建函数类型包括映射类型,然后使用工厂方法来创建函数类型:

\begin{cpp}
llvm::FunctionType *CGProcedure::createFunctionType(
ProcedureDeclaration *Proc) {
    llvm::Type *ResultTy = CGM.VoidTy;
    if (Proc->getRetType()) {
        ResultTy = mapType(Proc->getRetType());
    }
    auto FormalParams = Proc->getFormalParams();
    llvm::SmallVector<llvm::Type *, 8> ParamTypes;
    for (auto FP : FormalParams) {
        llvm::Type *Ty = mapType(FP);
        ParamTypes.push_back(Ty);
    }
    return llvm::FunctionType::get(ResultTy, ParamTypes,
                                    /*IsVarArgs=*/false);
}
\end{cpp}

根据函数类型,我们还创建了LLVM函数。这将函数类型与链接修改后的名称进行了关联:

\begin{cpp}
llvm::Function *
CGProcedure::createFunction(ProcedureDeclaration *Proc,
                            llvm::FunctionType *FTy) {
    llvm::Function *Fn = llvm::Function::Create(
        Fty, llvm::GlobalValue::ExternalLinkage,
        CGM.mangleName(Proc), CGM.getModule());
\end{cpp}

getModule()方法返回当前的LLVM模块,稍后我们将对其进行设置。

创建了函数后,可以添加更多关于它的信息:

\begin{itemize}
\item
首先,可以给出参数的名称,可使得IR更具可读性。

\item
其次,可以给函数和参数添加属性来指定一些特征。例如,通过引用传递的参数进行此操作。
\end{itemize}

在LLVM级别,这些参数是指针。但是从源语言设计来看,这些是非常受限的指针。与C++中的引用类似,总是需要为VAR参数指定一个变量。根据设计,我们知道这个指针永远不会为空,并且始终可解引用,所以可以读取指向的值,而不会有风险。而且,该指针不能传递——特别是,因为没有比函数调用更长的指针副本,所以不能捕获指针。

llvm::AttributeBuilder类用于为形式参数构建属性集。要获取参数类型的存储大小,可以简单地查询数据布局对象:

\begin{cpp}
    for (auto [Idx, Arg] : llvm::enumerate(Fn->args())) {
        FormalParameterDeclaration *FP =
            Proc->getFormalParams()[Idx];
        if (FP->isVar()) {
            llvm::AttrBuilder Attr(CGM.getLLVMCtx());
            llvm::TypeSize Sz =
                CGM.getModule()->getDataLayout().getTypeStoreSize(
                    CGM.convertType(FP->getType()));
            Attr.addDereferenceableAttr(Sz);
            Attr.addAttribute(llvm::Attribute::NoCapture);
            Arg.addAttrs(Attr);
        }
        Arg.setName(FP->getName());
    }
    return Fn;
}
\end{cpp}

这样,就创建了IR函数。下一节中,我们将把函数体的基本块添加到函数中。

\mySubsubsection{4.2.11.}{生成函数体}

我们几乎完成了为函数生成IR代码,只需要把这些片段组合在一起就可以生成函数了,包括它的函数体:

\begin{enumerate}
\item
给定一个来自tinylang的过程声明。首先,将创建函数类型和函数:

\begin{cpp}
void CGProcedure::run(ProcedureDeclaration *Proc) {
    this->Proc = Proc;
    Fty = createFunctionType(Proc);
    Fn = createFunction(Proc, Fty);
\end{cpp}

\item
接下来,将创建函数的第一个基本块,并使其成为当前块:

\begin{cpp}
    llvm::BasicBlock *BB = llvm::BasicBlock::Create(
        CGM.getLLVMCtx(), "entry", Fn);
    setCurr(BB);
\end{cpp}

\item
然后,必须逐一检查所有的形参。为了正确处理VAR参数,需要初始化FormalParams成员(在readVariable()中使用)。与局部变量相比,形参在第一个基本块中有一个值,所以些值必须已知:

\begin{cpp}
    for (auto [Idx, Arg] : llvm::enumerate(Fn->args())) {
        FormalParameterDeclaration *FP =
            Proc->getFormalParams()[Idx];
        FormalParams[FP] = &Arg;
        writeLocalVariable(Curr, FP, &Arg);
    }
\end{cpp}

\item
设置之后,可以调用emit()方法来生成IR代码:

\begin{cpp}
    auto Block = Proc->getStmts();
    emit(Proc->getStmts());
\end{cpp}

\item
生成IR代码后的最后一个块可能还没有密封,所以现在必须调用sealBlock()。tinylang中的过程可能有隐式返回,因此必须检查最后一个基本块是否有正确的结束符;否则,需要添加一个结束符:

\begin{cpp}
    if (!Curr->getTerminator()) {
        Builder.CreateRetVoid();
    }
    sealBlock(Curr);
}
\end{cpp}

\end{enumerate}

至此,我们已经完成了为函数生成IR代码的工作。但仍然需要创建LLVM模块,它将所有IR代码整合在一起。我们将在下一节中进行此操作。










================================================
FILE: content/part2/chapter4/3.tex
================================================

我们在LLVM模块中收集编译单元的所有函数和全局变量。为了简化IR生成过程,可以将前几节中的所有函数包装到代码生成器类中。为了获得一个工作的编译器,还需要定义想要为其生成代码的目标架构,并添加生成代码的通道。我们将在本章和接下来的几章中实现它,先从代码生成器开始。

\mySubsubsection{4.3.1.}{代码生成器}

IR模块包含为编译单元生成大括号内的所有元素。我们在全局级别遍历模块级别的声明,创建全局变量,并调用过程的代码生成。tinylang中的全局变量会映射到llvm::GobalValue类的实例。这种映射保存在全局变量中,可用于过程代码的生成:

\begin{cpp}
void CGModule::run(ModuleDeclaration *Mod) {
    for (auto *Decl : Mod->getDecls()) {
        if (auto *Var =
                llvm::dyn_cast<VariableDeclaration>(Decl)) {
            // Create global variables
            auto *V = new llvm::GlobalVariable(
                *M, convertType(Var->getType()),
                /*isConstant=*/false,
                llvm::GlobalValue::PrivateLinkage, nullptr,
                mangleName(Var));
            Globals[Var] = V;
        } else if (auto *Proc =
            llvm::dyn_cast<ProcedureDeclaration>(Decl)) {
            CGProcedure CGP(*this);
            CGP.run(Proc);
        }
    }
}
\end{cpp}

该模块还包含LLVMContext类,并缓存最常用的LLVM类型。后者需要初始化,例如:对于64位整数类型的初始化:

\begin{cpp}
Int64Ty = llvm::Type::getInt64Ty(getLLVMCtx());
\end{cpp}

CodeGenerator类初始化LLVM IR模块并调用该模块的代码生成,这个类必须了解要为哪个目标架构生成代码。该信息在驱动程序中的llvm::TargetMachine类中:

\begin{cpp}
std::unique_ptr<llvm::Module>
CodeGenerator::run(ModuleDeclaration *Mod,
                   std::string FileName) {
    std::unique_ptr<llvm::Module> M =
        std::make_unique<llvm::Module>(FileName, Ctx);
    M->setTargetTriple(TM->getTargetTriple().getTriple());
    M->setDataLayout(TM->createDataLayout());
    CGModule CGM(M.get());
    CGM.run(Mod);
    return M;
}
\end{cpp}

为了方便使用,必须为代码生成器引入一个工厂方法:

\begin{cpp}
CodeGenerator *
CodeGenerator::create(llvm::LLVMContext &Ctx,
                      llvm::TargetMachine *TM) {
    return new CodeGenerator(Ctx, TM);
}
\end{cpp}

CodeGenerator类提供了一个用于创建IR代码的接口,适合在编译器驱动程序中使用。在集成它之前,需要实现对机器代码生成的支持。

\mySubsubsection{4.3.2.}{初始化目标机型类}

现在,只有目标机型未知。在目标机型上,需要定义为其生成代码的CPU架构。对于每个CPU,都有一些特性会影响代码的生成过程。例如,CPU架构族中的一个较新的CPU可以支持向量指令。通过这些特性,我们可以切换使用向量指令的标志。支持从命令行设置这些选项,LLVM提供了一些支持代码。在Driver类中,可以包含以下头文件:

\begin{cpp}
#include "llvm/CodeGen/CommandFlags.h"
\end{cpp}

这个include将常见的命令行选项添加到编译器驱动程序中。许多LLVM工具也使用这些命令行选项,其好处是为用户提供了一个通用的接口,不过只缺少指定目标三元组的选项:

\begin{cpp}
static llvm::cl::opt<std::string> MTriple(
    "mtriple",
    llvm::cl::desc("Override target triple for module"));
\end{cpp}

让我们定义目标机型:

\begin{enumerate}
\item
要显示错误消息,必须将应用程序的名称传递给该函数:

\begin{cpp}
llvm::TargetMachine *
createTargetMachine(const char *Argv0) {
\end{cpp}

\item
首先,收集命令行提供的所有信息。这些是代码生成器的选项——CPU的名称和应该激活或停用的功能,以及目标的三个值:

\begin{cpp}
    llvm::Triple Triple = llvm::Triple(
        !MTriple.empty()
            ? llvm::Triple::normalize(MTriple)
            : llvm::sys::getDefaultTargetTriple());

    llvm::TargetOptions TargetOptions =
        codegen::InitTargetOptionsFromCodeGenFlags(Triple);
    std::string CPUStr = codegen::getCPUStr();
    std::string FeatureStr = codegen::getFeaturesStr();
\end{cpp}

\item
然后,必须在目标注册表中查找目标。若发生错误,将显示错误消息并退出。一个可能的错误是,用户指定的目标不支持:

\begin{cpp}
    std::string Error;
    const llvm::Target *Target =
        llvm::TargetRegistry::lookupTarget(
         codegen::getMArch(), Triple, Error);

    if (!Target) {
        llvm::WithColor::error(llvm::errs(), Argv0) << Error;
        return nullptr;
    }
\end{cpp}

\item
在Target类的帮助下,可以使用用户请求的所有已知选项来配置目标机型:

\begin{cpp}
    llvm::TargetMachine *TM = Target->createTargetMachine(
        Triple.getTriple(), CPUStr, FeatureStr,
        TargetOptions, std::optional<llvm::Reloc::Model>(
            codegen::getRelocModel()));
    return TM;
}
\end{cpp}

\end{enumerate}

有了目标机型实例,就可以生成相应CPU架构的IR代码。目前缺少的是向汇编文本的转换或目标代码文件的生成,我们将在下一节添加这种支持。

\mySubsubsection{4.3.3.}{生成汇编程文本和目标代码}

在LLVM中,IR代码通过通道运行。每次遍历都只执行一项任务,比如移除无效代码。我们将在第7章中了解更多关于通道的信息。输出汇编代码或目标文件也作为通道实现。让我们为它添加基本支持吧!

需要包含更多的LLVM头文件。首先,需要llvm::legacy::PassManager类来保存向文件发出代码的通道。还希望能够输出LLVM IR代码,因此还需要一个通道来生成它。最后,使用llvm::ToolOutputFile类进行文件操作:

\begin{cpp}
#include "llvm/IR/IRPrintingPasses.h"
#include "llvm/IR/LegacyPassManager.h"
#include "llvm/MC/TargetRegistry.h"
#include "llvm/Pass.h"
#include "llvm/Support/ToolOutputFile.h"
\end{cpp}

还需要另一个用于输出LLVM IR的命令行选项:

\begin{cpp}
static llvm::cl::opt<bool> EmitLLVM(
    "emit-llvm",
    llvm::cl::desc("Emit IR code instead of assembler"),
    llvm::cl::init(false));
\end{cpp}

最后,给输出文件一个名字:

\begin{cpp}
static llvm::cl::opt<std::string>
    OutputFilename("o",
                   llvm::cl::desc("Output filename"),
                   llvm::cl::value_desc("filename"));
\end{cpp}

新emit()方法中的第一个任务是,处理用户在命令行中没有给出的输出文件名。若从标准输入中读取输入,使用减号表示,则将结果输出到标准输出。ToolOutputFile类了解如何处理特殊的文件名:

\begin{cpp}
bool emit(StringRef Argv0, llvm::Module *M,
          llvm::TargetMachine *TM,
          StringRef InputFilename) {
    CodeGenFileType FileType = codegen::getFileType();
    if (OutputFilename.empty()) {
        if (InputFilename == "-") {
            OutputFilename = "-";
        }
\end{cpp}

否则,删除输入文件名的可能扩展名,并根据用户给出的命令行选项追加.ll、.s或.o作为扩展名。FileType选项在llvm/CodeGen/CommandFlags.inc头文件中定义,在前面包含了它。这个选项不支持发出IR代码,所以添加了new-emit-llvm选项,只有在与汇编文件类型一起使用时才有效:

\begin{cpp}
    else {
        if (InputFilename.endswith(".mod"))
            OutputFilename =
                InputFilename.drop_back(4).str();
        else
            OutputFilename = InputFilename.str();
        switch (FileType) {
            case CGFT_AssemblyFile:
                OutputFilename.append(EmitLLVM ? ".ll" : ".s");
                break;
            case CGFT_ObjectFile:
                OutputFilename.append(".o");
                break;
            case CGFT_Null:
                OutputFilename.append(".null");
                break;
        }
    }
}
\end{cpp}

一些平台区分文本和二进制文件,所以必须在打开输出文件时提供正确的标志:

\begin{cpp}
std::error_code EC;
sys::fs::OpenFlags OpenFlags = sys::fs::OF_None;
if (FileType == CGFT_AssemblyFile)
    OpenFlags |= sys::fs::OF_TextWithCRLF;
auto Out = std::make_unique<llvm::ToolOutputFile>(
    OutputFilename, EC, OpenFlags);
if (EC) {
    WithColor::error(llvm::errs(), Argv0)
        << EC.message() << '\n';
    return false;
}
\end{cpp}

现在,可以将所需的通道添加到PassManager。TargetMachine类有一个工具方法,用于添加所请求的类,所以只需要检查用户是否请求输出LLVM IR代码即可:

\begin{cpp}
    legacy::PassManager PM;
    if (FileType == CGFT_AssemblyFile && EmitLLVM) {
        PM.add(createPrintModulePass(Out->os()));
    } else {
        if (TM->addPassesToEmitFile(PM, Out->os(), nullptr,
                                    FileType)) {
            WithColor::error(llvm::errs(), Argv0)
                << "No support for file type\n";
            return false;
        }
    }
\end{cpp}

所有这些准备工作完成后,生成文件可以归结为一个函数调用:

\begin{cpp}
PM.run(*M);
\end{cpp}

若没有显式地请求保留该文件,ToolOutputFile类会自动删除该文件。这使得错误处理更容易,因为可能有许多地方需要处理错误。我们成功地生成了代码,所以想保留这个文件:

\begin{cpp}
Out->keep();
\end{cpp}

最后,必须向调用者报告成功与否:

\begin{cpp}
    return true;
}
\end{cpp}

使用llvm::Module调用emit()方法(通过调用CodeGenerator类创建了该方法),将按要求发出代码。假设将tinylang中的最大公约数算法存储在Gcd.mod文件中:

\begin{shell}
MODULE Gcd;

PROCEDURE GCD(a, b: INTEGER) : INTEGER;
VAR t: INTEGER;
BEGIN
    IF b = 0 THEN
        RETURN a;
    END;
    WHILE b # 0 DO
    t := a MOD b;
    a := b;
    b := t;
    END;
    RETURN a;
END GCD;

END Gcd.
\end{shell}

为了把这个翻译成Gcd.o目标文件,需要输入以下内容:

\begin{shell}
$ tinylang --filetype=obj Gcd.mod
\end{shell}

若想直接在屏幕上检查生成的IR代码,请输入以下命令:

\begin{shell}
$ tinylang --filetype=asm --emit-llvm -o - Gcd.mod
\end{shell}

以tinylang的当前实现状态,无法在tinylang中创建完整的程序,可以使用一个名为callgcd.c的C程序来测试生成的目标文件。注意,调用GCD函数时已经修改了名称:

\begin{cpp}
#include <stdio.h>

extern long _t3Gcd3GCD(long, long);

int main(int argc, char *argv[]) {
    printf("gcd(25, 20) = %ld\n", _t3Gcd3GCD(25, 20));
    printf("gcd(3, 5) = %ld\n", _t3Gcd3GCD(3, 5));
    printf("gcd(21, 28) = %ld\n", _t3Gcd3GCD(21, 28));
    return 0;
}
\end{cpp}

要使用clang编译并运行整个应用程序,需要输入以下命令:

\begin{shell}
$ tinylang --filetype=obj Gcd.mod
$ clang callgcd.c Gcd.o -o gcd
$ gcd
\end{shell}

让我们好好庆祝一下吧!至此,通过读取语言源码,并生成了汇编代码或目标文件,我们已经创建了一个完整的编译器。




















================================================
FILE: content/part2/chapter4/4.tex
================================================
本章中,了解了如何实现LLVM IR代码的代码生成器,基本块是保存所有指令和表示分支的重要数据结构。了解了如何为源语言的控制语句创建基本块,以及如何向基本块中添加指令。使用一种现代算法来处理函数中的局部变量,从而减少了IR代码。编译器的目标是为输入生成汇编文本或目标文件,因此还添加了一个简单的编译管道。有了这些知识,读者们将能够为自己的语言编译器生成LLVM IR代码和汇编程序文本或目标代码。

下一章中,将了解如何处理聚合数据结构,以及如何确保函数调用符合平台的规则。

================================================
FILE: content/part2/chapter5/0.tex
================================================

现在的高级语言通常使用聚合数据类型和面向对象编程(OOP)结构。LLVM IR对聚合数据类型有一定的支持,而OOP结构(如类)必须自己实现。添加聚合类型会引起传递聚合类型参数的问题。不同的平台有不同的规则(这也会体现在IR上),遵循调用约定还能够调用系统函数。

本章中,将了解如何将聚合数据类型和指针转换为LLVM IR,以及如何以符合系统的方式将参数传递给函数。还将了解如何在LLVM IR中实现类和虚函数。

本章中,将了解以下内容:

\begin{itemize}
\item
处理数组、结构体和指针

\item
正确获取应用程序的二进制接口(ABI)

\item
为类和虚函数创建IR代码
\end{itemize}

本章结束时,将了解为聚合数据类型和OOP构造创建LLVM IR的知识。还将了解如何根据平台的规则,传递聚合数据类型。



































================================================
FILE: content/part2/chapter5/1.tex
================================================
本章使用的代码在这里,\url{https://github.com/PacktPublishing/ Learn-LLVM-17/tree/main/Chapter05}。

================================================
FILE: content/part2/chapter5/2.tex
================================================
对于应用程序来说,只有INTEGER这样的基本类型是不够的。要表示矩阵或复数等数学对象,必须在已有数据类型的基础上构造新的数据类型。这些新数据类型通常称为聚合(aggregate)或组合(composite)。

数组是由相同类型的元素组成的序列。在LLVM中,数组总是静态的,所以元素的数量恒定。tinylang类型ARRAY [10] OF INTEGER或C类型long[10]在IR中表示如下:

\begin{shell}
[10 x i64]
\end{shell}

结构是不同类型的组合物。在编程语言中,通常用命名成员表示。例如,在tinylang中,结构可写成RECORD x: REAL;  color: INTEGER;  y: REAL;  END; C语言中同样的结构是struct \{ float x; long color; float y; \};。在LLVM IR中,只列出类型名:

\begin{shell}
{ float, i64, float }
\end{shell}

要访问成员,需要使用数字索引。与数组一样,第一个元素的索引号为0。

该结构的成员按照数据布局字符串中的规范在内存中排列。有关LLVM中数据布局字符串的更多信息,详见第4章。

若有必要,还会插入未使用的填充字节。若需要控制内存布局,可以使用打包结构,其中所有元素都具有1字节对齐方式。在C语言中,可以以下方式利用结构体的\_\_packed\_\_属性:

\begin{cpp}
struct __attribute__((__packed__)) { float x; long long color; float y; }
\end{cpp}

LLVM IR中的语法略有不同:

\begin{shell}
<{ float, i64, float }>
\end{shell}

加载到寄存器中,数组和结构体视为一个单元。例如,不可能将数组值寄存器\%x的单个元素引用为\%x[3]。这是由于SSA形式,不能判断\%x[i]和\%x[j]是否指的是同一个元素。相反,需要特殊的指令来提取和插入单元素值到数组中。要读取第二个元素,需要使用以下指令:

\begin{shell}
%el2 = extractvalue [10 x i64] %x, 1
\end{shell}

也可以更新一个元素,比如第一个元素:

\begin{shell}
%xnew = insertvalue [10 x i64] %x, i64 %el2, 0
\end{shell}

这两个指令也适用于结构体。例如,使用寄存器\%pt访问color成员:

\begin{shell}
%color = extractvalue { float, float, i64 } %pt, 2
\end{shell}

这两条指令都有一个重要的限制:索引必须是一个常量。对于结构体来说,索引号只是名称的替代,而C等语言没有动态计算结构成员名称的概念。对于数组来说,只是无法高效地实现。这两条指令在元素数量较少,且已知的特定情况下都有价值。例如,复数可以建模为两个浮点数组成的数组。传递这个数组是合理的,而且很清楚在计算过程中必须访问数组的哪一部分。

对于前端的一般使用,必须借助于指向内存的指针。LLVM中的所有全局值都用指针表示。下面将全局变量@arr声明为包含8个i64元素的数组,这等价于C语言中的long arr[8]声明:

\begin{shell}
define i64 @second() {
    %1 = load i64, ptr getelementptr inbounds ([8 x i64], ptr @arr, i64
    0, i64 1)
    ret i64 %1
}
\end{shell}

getelementptr指令是地址计算的主力,需要更详细的解释。第一个操作数[8 x i64]是指令所操作的基类型,第二个操作数ptr @arr指定基指针。请注意这里的差别:声明了一个包含八个元素的数组,由于所有全局值都视为指针,所以有一个指向数组的指针。在C的语法中,实际上使用long (*arr)[8]!其结果是,在对元素进行索引之前,必须先对指针解引用,如C语言中的arr[0][1]。第三个操作数i64 0对指针进行解引用,第四个操作数i64 1是元素索引,计算的结果是索引元素的地址。还需要注意的是,此指令不涉及内存存取。

除了结构体,索引参数不需要是常量,可以在循环中使用getelementptr指令来检索数组的元素。这里对结构体的处理有所不同:只能使用常量,且类型必须为i32。

有了这些知识,数组可以很容易地集成到第4章的代码生成器中,要创建这个类型,必须扩展convertType()方法。若Arr变量保存数组的类型标识符,并且假设数组中的元素数量为整数字面值,则可以向convertType()方法添加以下代码来处理数组:

\begin{cpp}
if (auto *ArrayTy =
            llvm::dyn_cast<ArrayTypeDeclaration>(Ty)) {
    llvm::Type *Component =
        convertType(ArrayTy->getType());
    Expr *Nums = ArrayTy->getNums();
    uint64_t NumElements =
        llvm::cast<IntegerLiteral>(Nums)
            ->getValue()
            .getZExtValue();
    llvm::Type *T =
        llvm::ArrayType::get(Component, NumElements);
    // TypeCache is a mapping between the original
    // TypeDeclaration (Ty) and the current Type (T).
    return TypeCache[Ty] = T;
}
\end{cpp}

该类型可用于声明全局变量。对于局部变量,需要为数组分配内存。我们在过程的第一个基本块中可以这样做:

\begin{cpp}
for (auto *D : Proc->getDecls()) {
    if (auto *Var =
            llvm::dyn_cast<VariableDeclaration>(D)) {
        llvm::Type *Ty = mapType(Var);
        if (Ty->isAggregateType()) {
            llvm::Value *Val = Builder.CreateAlloca(Ty);
            // The following method requires a BasicBlock (Curr),
            // a VariableDeclation (Var), and an llvm::Value (Val)
            writeLocalVariable(Curr, Var, Val);
        }
    }
}
\end{cpp}

为了读写一个元素,必须生成getelementptr指令,这会添加到emitExpr()(读取值)和emitStmt()(写入值)方法中。要读取数组的元素,首先读取变量的值,再处理变量的选择器。对于每个索引,计算表达式并存储其值。基于这个列表,计算引用元素的地址并加载值:

\begin{cpp}
auto &Selectors = Var->getSelectors();
for (auto I = Selectors.begin(), E = Selectors.end();
        I != E; ) {
    if (auto *IdxSel =
            llvm::dyn_cast<IndexSelector>(*I)) {
        llvm::SmallVector<llvm::Value *, 4> IdxList;
        while (I != E) {
            if (auto *Sel =
                    llvm::dyn_cast<IndexSelector>(*I)) {
                IdxList.push_back(emitExpr(Sel->getIndex()));
                ++I;
            } else
                break;
        }
        Val = Builder.CreateInBoundsGEP(Val->getType(), Val, IdxList);
        Val = Builder.CreateLoad(
            Val->getType(), Val);
    }
    // . . . Check for additional selectors and handle
    // appropriately by generating getelementptr and load.
    else {
        llvm::report_fatal_error("Unsupported selector");
    }
}
\end{cpp}

写入数组元素使用相同的代码,但不生成加载指令,可以使用指针作为存储指令中的目标。对于记录,可以使用类似的方法。记录成员的选择器包含名为Idx的常量字段索引,可把这个常量转换成一个LLVM常量值:

\begin{cpp}
llvm::Value *FieldIdx = llvm::ConstantInt::get(Int32Ty, Idx);
\end{cpp}

然后,在Builder.CreateGEP()方法中使用value,就像在数组中一样。

现在,应该知道如何将聚合数据类型转换为LLVM IR。以符合系统的方式传递这些类型值要非常谨慎,下一节将介绍如何正确地实现。















================================================
FILE: content/part2/chapter5/3.tex
================================================
通过向代码生成器添加数组和记录,有时生成的代码不会按预期执行,原因是我们忽略了平台的调用约定。对于同一个程序或库中的函数如何调用另一个函数,每个平台都定义了自己的规则。ABI文档中总结了这些规则,常见信息包括以下内容:

\begin{itemize}
\item
机器寄存器用于参数传递吗?如果是,是哪些?

\item
数组和结构这样的聚合类型如何传递给函数?

\item
如何处理返回值?
\end{itemize}

某些平台上,聚合类型间接传递,所以类型的副本会放在栈上,只有指向副本的指针作为参数传递。其他平台上,在寄存器中传递一个小的聚集(例如128或256位宽),只有超过该阈值时才使用间接参数传递。有些平台还使用浮点寄存器和向量寄存器来传递参数,而另一些平台则要求浮点值使用整数寄存器传递。

当然,这些都是些底层特性,但它会影响LLVM IR,明明已经在LLVM IR中定义了函数的所有参数类型?!事实证明,这还不够。为了理解这一点,来看一下复数。有些语言内置了复数的数据类型。例如,C99有float \_Complex(以及其他),旧版本的C没有复数类型,但可以定义struct complex \{float re, im;\}并在此类型上创建算术运算。这两种类型都可以映射到LLVM的\{float, float \} IR类型。

若ABI现在声明内置复数类型的值在两个浮点寄存器中传递,但用户定义的聚合总是间接传递,该函数给出的信息,不足以让LLVM决定如何传递这个特定的参数。但需要向LLVM提供更多的信息,而这些信息等级特定于ABI。

有两种方法可以将这些信息指定给LLVM:参数属性和类型重写,这取决于目标平台和代码生成器。最常用的参数属性如下所示:

\begin{itemize}
\item
inreg指定参数在寄存器中传递

\item
byval指定参数按值传递,该参数必须是指针类型。将指向的数据生成一个隐藏副本,并将该指针传递给调用函数。

\item
zeroext和signext指定传递的整数值是零或符号扩展。

\item
sret指定此形参保存一个指向内存的指针,该指针用于从函数返回聚合类型。
\end{itemize}

虽然所有代码生成器都支持zeroext、signext和sret属性,但只有一些支持inreg和byval。可以使用addAttr()将属性添加到函数的参数中。例如,要在参数Arg上设置inreg属性,可以使用以下方式:

\begin{cpp}
Arg->addAttr(llvm::Attribute::InReg);
\end{cpp}

若需要设置多个属性,可以使用llvm::AttrBuilder类。

提供信息的另一种方法是使用类型重写。使用这种方法,可以隐藏原始类型:

\begin{enumerate}
\item
拆分参数。例如,可以传递两个浮点参数,而不是传递一个复杂参数。

\item
将参数转换为不同的表示形式,例如:通过整数寄存器传递浮点值。
\end{enumerate}

要在不改变值位的情况下强制转换类型,可以使用bitcast指令。位转换指令可以操作简单的数据类型,如整数和浮点值。当通过整数寄存器传递浮点值时,必须将该浮点值强制转换为整数。在LLVM中,32位的浮点数表示为float,32位的整数表示为i32。可以通过以下方式将浮点值转换为整数:

\begin{shell}
%intconv = bitcast float %fp to i32
\end{shell}

此外,位转换指令要求两种类型具有相同的大小。

向参数添加属性或更改类型并不复杂,但如何知道需要实现什么呢?首先,应该大致了解目标平台上使用的调用约定。例如,Linux上的ELF ABI针对每种支持的CPU平台都有文档,可以查找文档并熟悉相关信息。

还有关于LLVM代码生成器需求的文档。信息的来源是clang实现,可以在\url{https://github.com/llvm/llvm-project/blob/main/clang/lib/CodeGen/TargetInfo.cpp}上找到。这个文件包含针对所有受支持平台的ABI操作,也是收集信息的地方。

本节中,了解了如何为函数调用生成与平台的ABI兼容的IR。下一节将为生成类和虚函数的IR创建不同的方法。






















































================================================
FILE: content/part2/chapter5/4.tex
================================================

许多现代编程语言使用类支持面向对象,类是高级语言结构。本节中,我们将探讨如何将类结构映射到LLVM IR中。

\mySubsubsection{5.4.1.}{实现单继承}

类是数据和方法的集合。一个类可以从另一个类继承,可能会添加更多的数据字段和方法,或者覆盖现有的虚函数。我们用Oberon-2中的类来说明这一点,Oberon-2也是一个很好的tinylang模型。Shape类定义了一个具有颜色和面积的抽象类型:

\begin{shell}
TYPE Shape = RECORD
                color: INTEGER;
                PROCEDURE (VAR s: Shape) GetColor(): INTEGER;
                PROCEDURE (VAR s: Shape) Area(): REAL;
             END;
\end{shell}

GetColor方法只返回颜号:

\begin{shell}
PROCEDURE (VAR s: Shape) GetColor(): INTEGER;
BEGIN RETURN s.color; END GetColor;
\end{shell}

抽象形状的面积无法计算,所以这是一种抽象的方法:

\begin{shell}
PROCEDURE (VAR s: Shape) Area(): REAL;
BEGIN HALT; END;
\end{shell}

Shape类型可以扩展为表示Circle类:

\begin{shell}
TYPE Circle = RECORD (Shape)
                radius: REAL;
                PROCEDURE (VAR s: Circle) Area(): REAL;
              END;
\end{shell}

对于一个圆,面积则可以计算:

\begin{shell}
PROCEDURE (VAR s: Circle) Area(): REAL;
BEGIN RETURN 2 * radius * radius; END;
\end{shell}

该类型也可以在运行时查询。若形状是Shape类型的变量,则可以这样制定类型测试:

\begin{shell}
IF shape IS Circle THEN (* … *) END;
\end{shell}

除了语法不同之外,其工作原理与C++非常相似。但显著区别是Oberon-2语法显式声明了this指针,将其称为方法的接收方。

要解决的基本问题是如何在内存中布局类,如何实现方法的动态调用和运行时类型检查。对于内存布局,Shape类只有一个数据成员,可以将其映射到相应的LLVM结构类型:

\begin{shell}
@Shape = type { i64 }
\end{shell}

Circle类添加了另一个数据成员,解决方案是在末尾附加新的数据成员:

\begin{shell}
@Circle = type { i64, float }
\end{shell}


原因是一个类可以有很多子类。使用这种策略,公共基类的数据成员始终具有相同的内存偏移量,并且使用相同的索引通过getelementptr指令访问相应的字段。

为了实现方法的动态调用,必须进一步扩展LLVM结构。若在Shape对象上调用Area()函数,则调用抽象方法,从而导致应用程序停止。若在Circle对象上调用,则调用相应的方法来计算圆的面积。另一方面,可以为这两个类的对象调用GetColor()函数。

实现这一点的基本思想是,将一个表与每个对象的函数指针关联起来。这里,表有两个信息:一个用于GetColor()方法,另一个用于Area()函数。Shape类和Circle类都有这样一个表。这些表在Area()函数的条目中有所不同,该函数根据对象的类型调用不同的代码。这个表称为虚函数表,通常缩写为vtable。

虚函数表本身没有用,必须把它和一个对象关联起来,总是添加指向虚函数表的指针,作为该结构的第一个数据成员。在LLVM级别,@Shape类型是这样的:

\begin{shell}
@Shape = type { ptr, i64 }
\end{shell}

@Circle类型也进行了类似的扩展。

得到的内存结构如图5.1所示:

\myGraphic{0.7}{content/part2/chapter5/images/1.png}{图5.1 - 类和虚函数表的内存布局}

LLVM IR中,Shape类的虚函数表可以可视化为如下表示,其中两个指针对应于GetColor()和GetArea()方法,如图5.1所示:

\begin{shell}
@ShapeVTable = constant { ptr, ptr } { GetColor(), Area() }
\end{shell}

此外,LLVM没有void指针,而是使用指向字节的指针。随着隐藏虚值表字段的引入,现在还需要有一种方法来初始化它。C++中,这是调用构造函数的一部分。在Oberon-2中,该字段在分配内存时自动初始化。

然后,通过以下步骤执行对方法的动态调用:

\begin{enumerate}
\item
通过getelementptr指令计算虚函数表指针的偏移量。

\item
加载指向虚参表的指针。

\item
计算函数在虚函数表中的偏移量。

\item
加载函数指针。

\item
使用调用指令通过指针间接调用函数。
\end{enumerate}

还可以在LLVM IR中可视化对虚拟方法(如Area())的动态调用。首先,从Shape类的相应指定位置加载一个指针。下面的load表示将指针加载到实际的虚函数表中,以便形成函数:

\begin{shell}
// Load a pointer from the corresponding location.
%ptrToShapeObj = load ptr, ...
// Load the first element of the Shape class.
%vtable = load ptr, ptr %ptrToShapeObj, align 8
\end{shell}

之后,getelementptr获取偏移量以调用Area():

\begin{shell}
%offsetToArea = getelementptr inbounds ptr, ptr %vtable, i64 1
\end{shell}

然后,加载指向Area()的函数指针:

\begin{shell}
%ptrToAreaFunction = load ptr, ptr %offsetToArea, align 8
\end{shell}

最后,通过指针调用Area()函数:

\begin{shell}
%funcCall = call noundef float %ptrToAreaFunction(ptr noundef
  nonnull align 8 dereferenceable(12) %ptrToShapeObj)
\end{shell}

即使在单继承的情况下,生成的LLVM IR也可能看起来非常冗长。尽管生成对方法的动态调用过程看起来效率不高,但大多数CPU架构只需两条指令就可以执行该动态调用。

此外,要将函数转换为方法,还需要对对象数据的引用,需要通过将指向数据的指针作为方法的第一个参数来实现的。在Oberon-2中,这是明确的接收者。在类似C++的语言中,是隐式this指针。

使用虚函数表,每个类在内存中都有唯一地址。这对运行时类型测试也有帮助吗?是的,但帮助有限。为了说明这个问题,用一个继承自Circle类的Ellipse类来扩展类层次,这不是数学意义上的is-a关系。

若有一个Shape类型的shape变量,则可以实现shape IS Circle类型的测试,将shape变量中存储的虚表指针与Circle类的虚表指针进行比较。只有当shape具有确切的Circle类型时,这种比较才会为true。但若shape是Ellipse类型,则比较返回false。但在需要Circle类型对象的所有地方,都可以使用Ellipse类型的对象。

解决方案是使用运行时类型信息扩展虚函数表,需要存储多少信息取决于源语言。为了支持运行时类型检查,存储一个指向基类虚函数表的指针就足够了,如图5.2所示:

\myGraphic{0.8}{content/part2/chapter5/images/2.png}{图5.2 - 支持简单类型测试的类和虚函数表布局}

若测试失败,则使用指向基类的虚函数表的指针重复测试。重复此操作,直到测试结果为true;若没有基类,则为false。与调用动态函数相比,类型测试是一个代价高昂的操作,最坏的情况下,继承层次结构可以向上回溯到根类。

若了解整个类层次结构,就有一种有效的方法:按照深度优先的顺序,对类层次结构的每个成员进行编号。类型测试变成了对一个数字或一个间隔进行比较,这可以在恒定的时间内完成。事实上,这就是LLVM自己的运行时类型测试的方法,我们在前一章中已经了解过了。

将运行时类型信息与虚函数表耦合是一种设计决策,要么是源语言强制要求的,要么只是作为实现细节。例如,语言在运行时支持反射,所以需要详细的运行时类型信息,并且数据类型没有vtable,将两者耦合就不是一个好主意。在C++中,耦合会导致具有虚函数(因此没有虚函数表)的类,没有运行时的类型数据。

通常,编程语言支持的接口是虚拟方法的集合。接口很重要,提供了有用的抽象。我们将在下一节中查看接口的可能实现方式。

\mySubsubsection{5.4.2.}{使用接口扩展单继承}

Java等语言支持接口,接口是抽象方法的集合,类似于没有数据成员、只定义了抽象方法的基类。接口带来了一个有趣的问题,实现接口的每个类,都可以在虚函数表中的不同位置拥有相应的方法。原因很简单,虚函数表中函数指针的顺序,是从语言源码中类定义中的函数顺序派生出来的。接口的定义与此无关,不同的顺序是标准。

接口中定义的方法可以有不同的顺序,可以将每个实现的接口的表附加到类中。对于接口的每个方法,这个表可以指定该方法在虚函数表中的索引,也可以指定存储在虚函数表中的函数指针的副本。若在接口上调用方法,则搜索接口对应的虚函数表,获取指向该函数的指针,并调用该方法。在Shape类中添加两个I1和I2接口会产生以下布局:

\myGraphic{0.7}{content/part2/chapter5/images/3.png}{图5.3 - 接口虚函数表的布局}

需要注意的是,必须找到合适的表。可以使用类似于运行时类型测试的方法:在接口虚函数表中执行线性搜索。可以为每个接口分配唯一编号(例如,一个内存地址),并使用这个编号来标识这个虚函数表。这种方案的缺点是显而易见的:通过接口调用方法比在类上调用相同的方法要花费更多的时间,但要解决这个问题并不容易。

一个好的方法是用哈希表代替线性搜索。在编译时,类实现的接口是已知的,以可以构造一个完美的哈希函数,将接口编号映射到接口的虚函数表。在构造过程中可能需要标识接口的唯一编号,还有其他方法可以计算唯一编号。若源码中的符号名称唯一,则可以计算该符号的加密哈希(例如MD5),并使用该哈希作为数字。计算在编译时进行,没有运行时成本。

结果比线性搜索快得多,只需要常数时间。不过,会涉及数的几个算术操作,比类类型方法调用要慢。

通常,接口也参与运行时类型测试,使列表搜索更长。若实现了哈希表方法,也可以用于运行时类型测试。

有些语言允许有多个父类。这对实现有一些有趣的挑战,我们将在下一节中了解这些。

\mySubsubsection{5.4.3.}{增加对多重继承的支持}

多重继承增加了另一个挑战。若一个类继承自两个或多个基类,则需要以一种仍然可以从方法访问的方式组合数据成员。与单继承情况类似,解决方案是附加所有数据成员,包括隐藏的虚函数表指针。

Circle类不仅是一个几何形状,而且是一个图形对象。为了对此建模,让Circle类继承Shape类和GraphicObj类。类布局中,Shape类中的字段首先出现,再追加GraphicObj类的所有字段,包括隐藏的虚函数表指针。之后,添加Circle类的新数据成员,得到如图5.4所示的整体结构:

\myGraphic{0.7}{content/part2/chapter5/images/4.png}{图5.4 - 具有多重继承的类和变量的布局}

这种方法有几个含义,可以有多个指向对象的指针。指向Shape或Circle类的指针指向对象的顶部,而指向GraphicObj类的指针指向该对象内部,即嵌入的GraphicObj对象的开始,在比较指针时必须考虑到这一点。

调用虚拟方法也会受到影响,若在GraphicObj类中定义了一个方法,则这个方法需要GraphicObj类的类布局。若在Circle类中没有重写此方法,则有两种可能。若方法调用通过指向GraphicObj实例的指针完成,可以在GraphicObj类的vtable中查找方法的地址并调用该函数。更复杂的情况是,若使用指向Circle类的指针调用该方法,可以在Circle类的虚函数表中查找方法的地址。调用的方法期望this指针是GraphicObj类的一个实例,所以也必须调整那个指针。因为我们知道GraphicObj类在Circle类中的偏移量,所以可以这样做。

若在Circle类中重写了GrapicObj方法,则通过指向Circle类的指针调用该方法,不需要做什么特殊操作。但若该方法是通过指向GraphicObj实例的指针调用,因为该方法需要一个指向Circle实例的this指针,就需要做一些调整。在编译时,因为不知道这个GraphicObj实例是否是多重继承层次结构的一部分,所以无法了解需要调整的部分。为了解决这个问题,我们将再调用方法之前,对this指针进行调整,并将每个函数指针一起存储在虚函数表中,如图5.5所示:

\myGraphic{0.7}{content/part2/chapter5/images/5.png}{图5.5 - vtable与this指针的调整}

一个方法调用现在的过程:

\begin{enumerate}
\item
在虚函数表中查找函数指针。

\item
调整this指针。

\item
调用该方法。
\end{enumerate}

这种方法也可用于实现接口。由于接口只有方法,所以每个实现的接口都会向对象添加一个新的虚函数表指针。这更容易实现,而且很可能更快,但每个对象实例增加了开销。

最坏的情况下,若类有一个64位的数据字段,但是实现了10个接口,则对象需要96字节的内存:8字节用于类本身的虚表指针,8字节用于数据成员,10 * 8字节用于每个接口的虚表指针。

为了支持对对象进行有意义的比较并执行运行时类型测试,需要首先规范指向对象的指针。若在虚函数表中添加一个字段,包含对象顶部的偏移量,则可以调整指针以指向实际对象。在Circle类的实值表中,该偏移量为0,但在嵌入GraphicObj类的实值表中则不是,这是否需要实现取决于源码语言的语义。

LLVM本身并不支持面向对象特性的特殊实现。如本节所示,可以使用可用的LLVM数据类型实现所有方法。正如具有单继承的LLVM IR示例一样,当涉及多继承时,IR可能会变得更加冗长。若想尝试一种新方法,一个好方法就是先用C语言做一个原型。所需的指针操作很快翻译成LLVM IR,但在高级语言中对功能的推理会更容易。

利用本节中获得的知识,可以在自己的代码生成器中实现将编程语言中常见的所有OOP结构简化为LLVM IR。现在,已经掌握了如何表示单继承、使用接口的单继承或内存中的多重继承,以及如何实现类型测试和如何查找虚拟函数,这些都是OOP语言的核心概念。



















































================================================
FILE: content/part2/chapter5/5.tex
================================================
在本章中,了解了如何将聚合数据类型和指针转换为LLVM IR代码,应用程序二进制接口的复杂性,将类和虚函数转换为LLVM IR的不同方法。有了本章的知识,您将能够为编程语言创建一个LLVM IR代码生成器。

下一章中,将了解一些有关IR生成的高级技术。异常处理在现代编程语言中相当常见,LLVM对此提供了一些支持。将类型信息添加到指针可以进行某些优化。最后,对许多开发人员来说,调试应用程序的能力必不可少,因此还需要在代码生成器中添加功能,以生成调试元数据。

================================================
FILE: content/part2/chapter6/0.tex
================================================

通过前面章节中介绍的IR生成,已经可以实现编译器中所需的大部分功能。本章中,将研究一些编译器中经常出现的高级主题。例如,许多现代语言都使用异常处理,将研究如何将其转换为LLVM IR。

为了支持LLVM优化器,使其能够在某些情况下生成更好的代码,必须向IR代码添加类型的元数据。添加的调试元数据,可使编译器用户能够使用源代码级调试工具进行调试。

本章中,将了解以下内容:

\begin{itemize}
\item
抛出和捕获异常:如何在编译器中实现异常处理

\item
为基于类型的别名分析生成元数据:将更多的元数据到LLVM IR,有助于LLVM更好地优化代码

\item
添加调试元数据:将实现向生成的IR代码添加调试信息所需的类
\end{itemize}

本章结束时,将了解异常处理,以及用于基于类型的别名分析和调试信息的元数据。























================================================
FILE: content/part2/chapter6/1.tex
================================================
LLVM IR 中的异常处理与平台支持密切相关,将使用libunwind来了解最常见的异常处理类型。C++可以充分发挥libunwind的潜力,先来看一个C++示例,其中bar()函数可以抛出一个int或double值:

\begin{cpp}
int bar(int x) {
    if (x == 1) throw 1;
    if (x == 2) throw 42.0;
    return x;
}
\end{cpp}

foo()函数调用bar(),但只处理抛出的int值,并且还声明只抛出int值:

\begin{cpp}
int foo(int x) {
    int y = 0;
    try {
        y = bar(x);
    }
    catch (int e) {
        y = e;
    }
    return y;
}
\end{cpp}

抛出异常需要两次调用运行时库,可以在bar()函数中看到。\_\_cxa\_allocate\_exception()为异常分配内存,这个函数接受要分配的字节数作为参数。异常负载(本例中的int或double值)被复制到分配的内存中,再通过\_\_cxa\_throw()引发异常。这个函数接受三个参数:指向已分配异常的指针、有关有效负载的类型信息,以及指向析构函数的指针(若异常有有效负载有的话)。\_\_cxa\_throw()函数启动堆栈展开过程,并且永远不会返回。LLVM IR中,对int值执行:

\begin{shell}
%eh = call ptr @__cxa_allocate_exception(i64 4)
store i32 1, ptr %eh
call void @__cxa_throw(ptr %eh, ptr @_ZTIi, ptr null)
unreachable
\end{shell}

\_ZTIi是描述int类型的类型信息。对于double类型,则为\_ZTId。

目前,还没有做特定于LLVM的工作。这一点在foo()函数中发生了变化,对bar()的调用可能引发异常。若是int类型异常,控制流必须转移到catch的IR代码。所以,必须使用invoke指令,而非call指令:

\begin{shell}
%y = invoke i32 @_Z3bari(i32 %x) to label %next
                                 unwind label %lpad
\end{shell}

这两条指令的区别在于,invoke有两个相关联的标签。在函数正常结束时,第一个标号表示执行继续,通常以ret指令结束。示例代码中,这个标签名为\%next。若发生异常,则在标号为\%lpad的“着陆垫”上继续执行。

著陆台是一个基本的块,必须以landingpad指令开始。landingpad指令向LLVM提供有关处理过的异常类型的信息。例如,一个着陆台可能是这样的:

\begin{shell}
lpad:
%exc = landingpad { ptr, i32 }
            cleanup
            catch ptr @_ZTIi
            filter [1 x ptr] [ptr @_ZTIi]
\end{shell}

这里有三种可能的操作类型:

\begin{itemize}
\item
cleanup:表示存在用于清理当前状态的代码,用于调用局部对象的析构函数。若此标记存在,则在堆栈展开期间调用着陆垫指令。

\item
catch:是类型-值对的列表,表示可以处理的异常类型。若在此列表中找到抛出的异常类型,则调用着陆垫指令。在foo()函数的情况下,该值是指向int类型的C++运行时类型信息的指针,类似于\_\_cxa\_throw()函数的参数。

\item
filter:指定一个异常类型数组。若在数组中找不到当前异常的异常类型,则调用着陆垫指令,用于实现throw()规范。对于foo()函数,数组只有一个成员——int类型的类型信息。
\end{itemize}

着陆平垫指令的结果类型是\{ptr, i32 \}结构。第一个元素是指向抛出异常的指针,第二个元素是类型选择器。从结构体中进行提取:

\begin{shell}
%exc.ptr = extractvalue { ptr, i32 } %exc, 0
%exc.sel = extractvalue { ptr, i32 } %exc, 1
\end{shell}

类型选择器是一个数字,可以帮助确定为什么要调用着陆垫指令。若当前异常类型与landingpad指令的catch部分给出的异常类型匹配,则该值为正值。若当前异常类型不匹配过滤部分给出的值,则值为负。若应该调用清理代码,则为0。

类型选择器是类型信息表的偏移量,由着陆垫指令的catch和filter部分给出的值构造而成。优化过程中,多个着陆垫指令可以合并为一个,所以在IR级别上该表的结构是未知的。要检索给定类型的类型选择器,需要调用内部的@llvm.eh.typeid.for的函数。需要它来检查类型选择器值是否与int的类型信息相对应,以便执行catch (int e) \{\}块中的代码:

\begin{shell}
%tid.int = call i32 @llvm.eh.typeid.for(ptr @_ZTIi)
%tst.int = icmp eq i32 %exc.sel, %tid.int
br i1 %tst.int, label %catchint, label %filterorcleanup
\end{shell}

异常的处理是通过调用\_\_cxa\_begin\_catch()和\_\_cxa\_end\_catch()实现的。\_\_cxa\_begin\_catch()函数需要一个参数——当前异常——是着陆垫指令返回的值之一,返回一个指向异常负载的指针——本例中是int值。

\_\_cxa\_end\_catch()函数标志着异常处理的结束,并释放用\_\_cxa\_allocate\_exception()分配的内存。注意,若在catch块中抛出另一个异常,则运行时行为会复杂得多。异常处理方法如下:

\begin{shell}
catchint:
%payload = call ptr @__cxa_begin_catch(ptr %exc.ptr)
%retval = load i32, ptr %payload
call void @__cxa_end_catch()
br label %return
\end{shell}

若当前异常的类型与throws()声明中的列表不匹配,则调用意外异常处理程序,需要再次检查类型选择器:

\begin{shell}
filterorcleanup:
%tst.blzero = icmp slt i32 %exc.sel, 0
br i1 %tst.blzero, label %filter, label %cleanup
\end{shell}

若类型选择器的值小于0,则调用该处理程序:

\begin{shell}
filter:
call void @__cxa_call_unexpected(ptr %exc.ptr) #4
unreachable
\end{shell}

同样,该处理程序也不会返回。

这种情况下不需要清理,所有清理代码要做的就是恢复堆栈展开的执行:

\begin{shell}
cleanup:
resume { ptr, i32 } %exc
\end{shell}

libunwind驱动堆栈展开过程,但没有绑定到语言,依赖于语言的处理在个性函数中完成。对于Linux上的C++,个性函数(personality function)称为\_\_gxx\_personality\_v0()。根据平台或编译器的不同,这个名称可能有所不同。每个需要参与堆栈展开的函数都有一个个性函数。此个性函数分析函数是否捕获异常、是否具有不匹配的筛选器列表或是否需要清理调用,将这些信息返回给解卷器,解卷器采取相应的行动。在LLVM IR中,个性函数的指针是作为函数定义的一部分给出的:

\begin{shell}
define i32 @_Z3fooi(i32) personality ptr @__gxx_personality_v0
\end{shell}

至此,异常处理工具就完成了。

要在编译器中为编程语言使用异常处理,最简单的策略是利用现有的C++运行时函数。这还有一个好处,就是异常可以与C++互操作。缺点是将一些C++运行时绑定到语言的运行时中,尤其是内存管理。若想避免这种情况,则需要创建自己的\_cxa\_函数。不过,想必还是会想要使用libunwind,因为它提供了堆栈解旋机制:

\begin{enumerate}
\item
来看看如何创建这个IR,在第2章中创建了calc表达式编译器。将扩展表达式编译器的代码生成器,以便在执行除零操作时引发并处理异常。生成的IR将检查除法的除数是否为0。若为true,则会引发异常。还将在函数中添加一个着陆垫指令,用来捕获异常并打印Divide by 0 !到控制台,并结束计算。这个简单的例子中,不需要使用异常处理,但可以让专注于代码生成过程。必须将所有代码添加到CodeGen.cpp文件中,首先添加所需的新字段和一些辅助方法,需要存储\_\_cxa\_allocate\_exception()和\_\_cxa\_throw()函数的LLVM声明,其由函数类型和函数本身组成。需要一个GlobalVariable实例来保存类型信息,还需要引用控制着陆垫指令的基本块和包含不可达指令的基本块:

\begin{cpp}
GlobalVariable *TypeInfo = nullptr;
FunctionType *AllocEHFty = nullptr;
Function *AllocEHFn = nullptr;
FunctionType *ThrowEHFty = nullptr;
Function *ThrowEHFn = nullptr;
BasicBlock *LPadBB = nullptr;
BasicBlock *UnreachableBB = nullptr;
\end{cpp}

\item
还要添加一个新辅助函数,来比较两个值的IR。createICmpEq()函数接受Left和Right值作为参数进行比较,创建一个比较指令来测试值是否相等,并为两个基本块创建一个分支指令,用于相等和不相等的情况,这两个基本块通过TrueDest和FalseDest参数中的引用返回。此外,新基本块的标签可以在TrueLabel和FalseLabel参数中给出:

\begin{cpp}
void createICmpEq(Value *Left, Value *Right,
                    BasicBlock *&TrueDest,
                    BasicBlock *&FalseDest,
                    const Twine &TrueLabel = "",
                    const Twine &FalseLabel = "") {
    Function *Fn =
        Builder.GetInsertBlock()->getParent();
    TrueDest = BasicBlock::Create(M->getContext(),
                                    TrueLabel, Fn);
    FalseDest = BasicBlock::Create(M->getContext(),
                                    FalseLabel, Fn);
    Value *Cmp = Builder.CreateCmp(CmpInst::ICMP_EQ,
                                    Left, Right);
    Builder.CreateCondBr(Cmp, TrueDest, FalseDest);
}
\end{cpp}

\item
要使用运行时中的函数,需要创建几个函数声明。在LLVM中,函数类型给出了签名,并且必须构造函数本身,使用createFunc()方法创建这两个对象。函数需要引用FunctionType和Function指针、新声明函数的名称和结果类型。参数类型列表是可选的,变量参数列表标志设置为false,表示参数列表中没有变量部分:

\begin{cpp}
void createFunc(FunctionType *&Fty, Function *&Fn,
                const Twine &N, Type *Result,
                ArrayRef<Type *> Params = None,
                bool IsVarArgs = false) {
    Fty = FunctionType::get(Result, Params, IsVarArgs);
    Fn = Function::Create(
        Fty, GlobalValue::ExternalLinkage, N, M);
}
\end{cpp}
\end{enumerate}

完成这些准备工作后,就可以生成抛出异常的IR了。

\mySubsubsection{6.1.1.}{抛出异常}

为了生成引发异常的IR代码,将添加addThrow()方法。这个新方法需要初始化新字段,然后通过\_\_cxa\_throw()函数生成引发异常的IR。所引发异常的有效负载为int类型,可以设置为任意值。下面是需要编写的代码:

\begin{enumerate}
\item
新的addThrow()方法首先检查TypeInfo字段是否已初始化。若还没有初始化,则会创建一个i8指针类型的全局外部常量\_ZTIi。这代表了描述C++ int类型的元数据:

\begin{cpp}
void addThrow(int PayloadVal) {
    if (!TypeInfo) {
        TypeInfo = new GlobalVariable(
        *M, Int8PtrTy,
        /*isConstant=*/true,
        GlobalValue::ExternalLinkage,
        /*Initializer=*/nullptr, "_ZTIi");
\end{cpp}

\item
初始化继续使用辅助createFunc()方法为\_\_cxa\_allocate\_exception()和\_\_cxa\_throw()函数创建IR声明:

\begin{cpp}
        createFunc(AllocEHFty, AllocEHFn,
                "__cxa_allocate_exception", Int8PtrTy,
                {Int64Ty});
        createFunc(ThrowEHFty, ThrowEHFn, "__cxa_throw",
                VoidTy,
                {Int8PtrTy, Int8PtrTy, Int8PtrTy});
\end{cpp}

\item
使用异常处理的函数需要一个个性函数,有助于堆栈展开。添加了IR代码来声明C++库中的\_\_gxx\_personality\_v0()个性函数,并将其设置为当前函数的个性例程。当前函数没有存储为一个字段,可以使用Builder实例来查询当前的基本块,其将函数存储为Parent字段:

\begin{cpp}
        FunctionType *PersFty;
        Function *PersFn;
        createFunc(PersFty, PersFn,
                    "__gxx_personality_v0", Int32Ty, std::nulopt,
                    true);
        Function *Fn =
        Builder.GetInsertBlock()->getParent();
        Fn->setPersonalityFn(PersFn);
\end{cpp}

\item
接下来,必须创建并填充着陆台指令的基本块。首先,需要保存指向当前基本块的指针,再创建一个新的基本块,在构建器中进行设置,以便可以用作插入指令的基本块,并调用addLandingPad()方法。该方法生成用于处理异常的IR代码,将在下一节捕获异常中进行描述。这段代码填充了着陆垫指令的基本块:

\begin{cpp}
        BasicBlock *SaveBB = Builder.GetInsertBlock();
        LPadBB = BasicBlock::Create(M->getContext(),
                                    "lpad", Fn);
        Builder.SetInsertPoint(LPadBB);
        addLandingPad();
\end{cpp}

\item
初始化部分通过创建保存不可达指令的基本块来完成,并创建基本块并将其设置为构建器中的插入点。然后,可以将不可达指令添加到其中,再将构建器的插入点设置回已保存的SaveBB实例,以便将以下IR添加到正确的基本块中:

\begin{cpp}
        UnreachableBB = BasicBlock::Create(
        M->getContext(), "unreachable", Fn);
        Builder.SetInsertPoint(UnreachableBB);
        Builder.CreateUnreachable();
        Builder.SetInsertPoint(SaveBB);
    }
\end{cpp}

\item
要抛出异常,通过调用\_\_cxa\_allocate\_exception()函数为异常和负载分配内存。我们的有效负载是C++ int类型,通常大小为4字节,再为size创建一个常量无符号值,并将其作为参数调用函数。函数类型和函数声明已经初始化,所以只需要创建调用指令:

\begin{cpp}
    Constant *PayloadSz =
        ConstantInt::get(Int64Ty, 4, false);
    CallInst *EH = Builder.CreateCall(
        AllocEHFty, AllocEHFn, {PayloadSz});
\end{cpp}

\item
接下来,将PayloadVal值存储在分配的内存中,需要通过调用ConstantInt::get()函数创建一个LLVM IR常量。所述指向所分配内存的指针为i8指针类型;为了存储i32类型的值,需要创建一个bitcast指令来强制转换该类型:

\begin{cpp}
    Value *PayloadPtr =
        Builder.CreateBitCast(EH, Int32PtrTy);
    Builder.CreateStore(
        ConstantInt::get(Int32Ty, PayloadVal, true),
        PayloadPtr);
\end{cpp}

\item
最后,必须通过调用\_\_cxa\_throw()函数来抛出异常。由于这个函数引发了一个异常,该异常也在同一个函数中处理,所以需要使用invoke指令,而非call指令。与调用指令不同,因为它有两个后继基本块,所以调用指令会结束一个基本块,这些是UnreachableBB和LPadBB基本块。若函数没有引发异常,控制流将转移到UnreachableBB基本块。由于\_\_cxa\_throw()函数的设计,因为控制流会转移到LPadBB基本块来处理异常,所以这种情况永远不会发生。这样就完成了addThrow()方法的实现:

\begin{cpp}
    Builder.CreateInvoke(
    ThrowEHFty, ThrowEHFn, UnreachableBB, LPadBB,
    {EH,
     ConstantExpr::getBitCast(TypeInfo, Int8PtrTy),
     ConstantPointerNull::get(Int8PtrTy)});
}
\end{cpp}

\end{enumerate}

接下来,将添加代码来生成处理异常的IR。

\mySubsubsection{6.1.2.}{捕捉异常}

要生成捕获异常的IR代码,必须添加addLandingPad()方法。生成的IR从异常中提取类型信息。若匹配C++ int类型,则通过打印Divide by 0!到控制台来处理异常并从函数中返回。若类型不匹配,只需执行resume指令,该指令将控制转移回运行时。由于调用层次结构中没有其他函数来处理此异常,因此将终止应用程序的运行。以下步骤描述了生成用于捕获异常的IR:

\begin{enumerate}
\item
生成的IR中,需要从C++运行时库中调用\_\_cxa\_begin\_catch()和\_\_cxa\_end\_catch()函数。为了输出错误消息,将从C运行时库生成对puts()函数的调用。此外,为了从异常中获取类型信息,必须生成对llvm.eh.typeid.for指令的调用。还需要FunctionType和Function实例,我们将利用createFunc()方法来创建它们:

\begin{cpp}
void addLandingPad() {
    FunctionType *TypeIdFty; Function *TypeIdFn;
    createFunc(TypeIdFty, TypeIdFn,
                "llvm.eh.typeid.for", Int32Ty,
                {Int8PtrTy});
    FunctionType *BeginCatchFty; Function *BeginCatchFn;
    createFunc(BeginCatchFty, BeginCatchFn,
                "__cxa_begin_catch", Int8PtrTy,
                {Int8PtrTy});
    FunctionType *EndCatchFty; Function *EndCatchFn;
    createFunc(EndCatchFty, EndCatchFn,
                "__cxa_end_catch", VoidTy);
    FunctionType *PutsFty; Function *PutsFn;
    createFunc(PutsFty, PutsFn, "puts", Int32Ty,
                {Int8PtrTy});
\end{cpp}

\item
着陆台指令是我们生成的第一条指令,结果类型是一个包含i8指针和i32类型字段的结构体。这个结构是通过调用StructType::get()函数生成的。此外,由于需要处理C++ int类型的异常,还需要将this作为子句添加到landingpad指令中,该指令必须是i8指针类型的常量,所以需要生成一个位转换指令来将TypeInfo值转换为这种类型,再将指令返回的值存储在Exc变量中,供后续使用:

\begin{cpp}
    LandingPadInst *Exc = Builder.CreateLandingPad(
        StructType::get(Int8PtrTy, Int32Ty), 1, "exc");
    Exc->addClause(
        ConstantExpr::getBitCast(TypeInfo, Int8PtrTy));
\end{cpp}

\item
接下来,从返回值中提取类型选择器。调用llvm.eh.typeid.for指令,检索TypeInfo字段的类型ID,表示C++ int类型。使用这个IR,已经生成了两个值。我们需要进行比较,以决定是否可以处理异常:

\begin{cpp}
    Value *Sel =
        Builder.CreateExtractValue(Exc, {1}, "exc.sel");
    CallInst *Id =
        Builder.CreateCall(TypeIdFty, TypeIdFn,
                            {ConstantExpr::getBitCast(
                                TypeInfo, Int8PtrTy)});
\end{cpp}

\item
要生成用于比较的IR,必须调用createICmpEq()函数。这个函数还生成了两个基本块,将它们存储在TrueDest和FalseDest变量中:

\begin{cpp}
    BasicBlock *TrueDest, *FalseDest;
    createICmpEq(Sel, Id, TrueDest, FalseDest, "match",
                "resume");
\end{cpp}

\item
若这两个值不匹配,控制流将在false基本块处继续。这个基本块只包含一个resume指令,将控制权交还给C++运行时:

\begin{cpp}
    Builder.SetInsertPoint(FalseDest);
    Builder.CreateResume(Exc);
\end{cpp}

\item
若这两个值相等,则控制流在TrueDest基本块处继续。生成IR代码,以便从存储在Exc变量中的着陆平台指令的返回值中,提取指向异常的指针。生成对\_\_cxa\_begin\_catch()函数的调用,将指向异常的指针作为参数传递。这表示在运行时开始处理异常:

\begin{cpp}
    Builder.SetInsertPoint(TrueDest);
    Value *Ptr =
        Builder.CreateExtractValue(Exc, {0}, "exc.ptr");
    Builder.CreateCall(BeginCatchFty, BeginCatchFn,
                        {Ptr});
\end{cpp}

\item
再用puts()函数来处理异常,将消息输出到控制台,这里通过调用CreateGlobalStringPtr()函数生成一个指向该字符串的指针,在生成的调用中将该指针作为参数传递给puts()函数:

\begin{cpp}
    Value *MsgPtr = Builder.CreateGlobalStringPtr(
        "Divide by zero!", "msg", 0, M);
    Builder.CreateCall(PutsFty, PutsFn, {MsgPtr});
\end{cpp}

\item
现在已经处理了异常,必须生成对\_\_cxa\_end\_catch()函数的调用,以通知运行时有关它的信息。最后,从函数返回一个aret指令:

\begin{cpp}
    Builder.CreateCall(EndCatchFty, EndCatchFn);
    Builder.CreateRet(Int32Zero);
}
\end{cpp}

\end{enumerate}

使用addThrow()和addLandingPad()函数,可以生成引发异常和处理异常的IR,但仍需要添加IR来检查除数是否为0。

我们将在下一节中讨论这一点。

\mySubsubsection{6.1.3.}{将异常处理代码集成到应用程序中}

除法的IR是在visit(BinaryOp \&)方法中生成的,必须生成一个IR来将除数与0进行比较,而不是仅仅生成一个sdiv指令。若除数为0,则控制流继续在基本块中运行,从而引发异常;否则,控制流将在带有sdiv指令的基本块中继续。在createICmpEq()和addThrow()函数的帮助下,可以编写如下代码:

\begin{cpp}
    case BinaryOp::Div:
        BasicBlock *TrueDest, *FalseDest;
        createICmpEq(Right, Int32Zero, TrueDest,
                     FalseDest, "divbyzero", "notzero");
        Builder.SetInsertPoint(TrueDest);
        addThrow(42); // Arbitrary payload value.
        Builder.SetInsertPoint(FalseDest);
        V = Builder.CreateSDiv(Left, Right);
        break;
\end{cpp}

代码生成部分现在已经完成。要构建应用程序,必须切换到构建目录并运行ninja工具:

\begin{shell}
$ ninja
\end{shell}

构建完成后,可以用with a: 3/a表达式检查生成的IR:

\begin{shell}
$ src/calc "with a: 3/a"
\end{shell}

将看到抛出和捕获异常所需的IR。

生成的IR现在依赖于C++运行时,链接所需库的最简单方法是使用clang++编译器。使用表达式计算器的运行时函数将rtcalc.c文件重命名为rtcalc.cpp,并在文件中的每个函数前面添加extern "C"。然后,使用llc工具将生成的IR转换为目标文件,并使用clang++编译器创建可执行文件:

\begin{shell}
$ src/calc "with a: 3/a" | llc -filetype obj -o exp.o
$ clang++ -o exp exp.o ../rtcalc.cpp
\end{shell}

现在,可以用不同的值对程序进行测试:

\begin{shell}
$ ./exp
Enter a value for a: 1
The result is: 3
$ ./exp
Enter a value for a: 0
Divide by zero!
\end{shell}

第二次运行时,输入为0,这会引发异常——像预期的那样工作了!

本节中,了解了如何引发和捕获异常。生成IR的代码可以用作其他编译器的蓝图,所使用的类型信息和catch子句的数量取决于编译器的输入,但需要生成的IR仍然遵循本节中提供的模式。

添加元数据是向LLVM提供进一步信息的另一种方式。下一节中,将添加类型元数据,以便在某些情况下支持LLVM优化器。











































================================================
FILE: content/part2/chapter6/2.tex
================================================

两个指针可能指向同一个存储单元,在这一点上它们彼此别名。内存在LLVM模型中没有类型,这使得优化器很难确定两个指针是否相互别名。若编译器可以证明两个指针不会相互别名,就可以进行更多的优化。在下一节中,我们将更深入地研究这个问题,并研究在实现这种方法之前添加元数据将有什么好处。

\mySubsubsection{6.2.1.}{理解对元数据的需求}

为了演示这个问题,来看看下面的函数:

\begin{cpp}
void doSomething(int *p, float *q) {
    *p = 42;
    *q = 3.1425;
}
\end{cpp}

优化器不能决定指针p和q是否指向相同的内存单元。优化过程中,可以执行一个重要的分析,称为别名分析。若p和q指向相同的存储单元,其互为别名。此外,若优化器可以证明两个指针永远不会相互别名,这就提供了优化机会。例如,在doSomething()函数中,存储可以在不改变结果的情况下重新排序。

此外,若一种类型的变量可以是另一种不同类型变量的别名,则取决于源语言的定义。注意,语言也可能包含不基于类型的别名假设的表达式——例如,不相关类型之间的类型强制转换。

LLVM开发人员选择的解决方案是在加载和存储指令中添加元数据。添加的元数据有两个目的:

\begin{itemize}
\item
首先,定义了类型层次结构,在此基础上类型可以别名另一个类型

\item
其次,描述了加载或存储指令中的内存访问
\end{itemize}

来看一下C中的类型层次结构。每种类型的层次结构都从一个根节点开始,可以命名,也可以匿名。LLVM假设具有相同名称的根节点描述相同类型的层次结构,可以在相同的LLVM模块中使用不同的类型层次结构,并且LLVM可以安全地假设这些类型可以别名。在根节点下面是标量类型的节点,聚合类型的节点不添加到根节点,但会引用标量类型和其他聚合类型。Clang定义C语言的层次结构如下:

\begin{itemize}
\item
根节点称为简单的C/C++ TBAA。

\item
根节点下面是用于字符类型的节点。这是C语言中的一种特殊类型,所有指针都可以转换为指向char的指针。

\item
char节点下面是其他标量类型的节点和所有指针的类型,称为any指针。
\end{itemize}

除此之外,聚合类型可定义为成员类型和偏移量的序列。

这些元数据定义会添加到加载和存储指令的访问标记中。访问标记由三部分组成:基本类型、访问类型和偏移量。根据基本类型的不同,访问标签描述内存访问有两种可能的方式:

\begin{enumerate}
\item
若基类型是聚合类型,则访问标记描述具有必要访问类型的struct成员的内存访问,并位于给定的偏移量。

\item
若基类型是标量类型,则访问类型必须与基类型相同,并且偏移量必须为0。
\end{enumerate}

有了这些定义,就可以在访问标记上定义一个关系,用于计算两个指针是否可以相互别名。来仔细看看(基类型,偏移量)元组的直接父对象的选项:

\begin{enumerate}
\item
若基类型是标量类型且偏移量为0,则直接父类型为(父类型,0),父类型为父节点的类型,如类型层次结构中定义的那样。若偏移量不为0,则父级未定义。

\item
若基类型是聚合类型,则(基类型,偏移量)元组的直接父级是(新类型,新偏移量)元组,新类型是偏移量处成员的类型。新偏移量是新类型的偏移量,调整到其新起点。
\end{enumerate}

这个关系的传递闭包就是父关系。两个内存访问(基类型1,访问类型1,偏移量1)和(基类型2,访问类型2,偏移量2),若(基类型1,偏移量1)和(基类型2,偏移量2)在父关系中相关,则相互别名,反之亦然。

用一个例子来说明这一点:

\begin{cpp}
struct Point { float x, y; }
void func(struct Point *p, float *x, int *i, char *c) {
    p->x = 0; p->y = 0; *x = 0.0; *i = 0; *c = 0;
}
\end{cpp}

当使用标量类型的内存访问标记定义时,i参数的访问标记是(int, int, 0),而c参数的访问标记是(char, char, 0)。类型层次结构中,int类型的节点的父节点是char节点,所以(int, 0)的直接父指针是(char, 0),并且两个指针都可以别名。对于x和c参数也是如此,但x和i参数是不相关的,所以它们不会相互别名。对结构体Point的y成员的访问是(Point, float, 4),其中4是该结构体中y成员的偏移量。(Point, 4)的直接父变量是(float, 0),所以对p->y和x的访问可以别名;同理,参数c也可以使用别名。

\mySubsubsection{6.2.2.}{LLVM中创建TBAA元数据}

创建元数据,必须使用llvm::MDBuilder类,在llvm/IR/MDBuilder.h头文件中声明。数据本身存储在llvm::MDNode和llvm::MDString类的实例中,使用builder类可以避免我们了解构造的内部细节。

通过createTBAARoot()方法创建根节点,该方法需要将类型层次结构的名称作为参数,并返回根节点。可以使用createAnonymousTBAARoot()方法创建一个匿名的唯一根节点。

使用createTBAAScalarTypeNode()方法将标量类型添加到层次结构中,该方法将类型的名称和父节点作为参数。

另一方面,为聚合类型添加类型节点稍微复杂一些。createTBAAStructTypeNode()方法接受类型的名称和字段列表作为参数。具体来说,字段以std::pair<llvm::MDNode*, uint64\_t>实例的形式给出,其中第一个元素表示成员的类型,第二个元素表示struct中的偏移量。

使用createTBAAStructTagNode()方法创建访问标记,该方法将基类型、访问类型和偏移量作为参数。

最后,元数据必须附加到加载或存储指令。Instruction类包含一个名为setMetadata()的方法,该方法用于添加各种基于类型的别名分析元数据。第一个参数必须是llvm::LLVMContext::MD\_tbaa类型,第二个参数必须是访问标签。

有了这些知识,就可以向tinylang添加用于基于类型的别名分析(TBAA)的元数据了。

\mySubsubsection{6.2.3.}{tinylang中添加TBAA元数据}

为了支持TBAA,必须添加一个新的CGTBAA类,该类负责生成元数据节点。此外,我们使CGTBAA类成为CGModule类的成员,称其为TBAA。

每条加载和存储指令都必须加注解,为此在CGModule类中创建了一个名为decorateInst()的新函数,这个函数尝试创建标记访问信息。若成功,则元数据将添加到相应的加载或存储指令。此外,这种设计还允许在不需要的情况下关闭元数据生成过程,例如在构建中关闭优化:

\begin{cpp}
void CGModule::decorateInst(llvm::Instruction *Inst,
                            TypeDeclaration *Type) {
    if (auto *N = TBAA.getAccessTagInfo(Type))
        Inst->setMetadata(llvm::LLVMContext::MD_tbaa, N);
}
\end{cpp}

我们将新CGTBAA类的声明放在include/tinylang/CodeGen/CGTBAA.h头文件中,并将定义放在lib/CodeGen/CGTBAA.cpp文件中。除了AST定义,头文件还需要包含定义元数据节点和构建器的文件:

\begin{cpp}
#include "tinylang/AST/AST.h"
#include "llvm/IR/MDBuilder.h"
#include "llvm/IR/Metadata.h"
\end{cpp}

CGTBAA类需要存储一些数据成员:

\begin{enumerate}
\item
首先,需要缓存类型层次结构的根:

\begin{cpp}
class CGTBAA {
    llvm::MDNode *Root;
\end{cpp}

\item
为了构造元数据节点,需要一个MDBuilder类的实例:

\begin{cpp}
    llvm::MDBuilder MDHelper;
\end{cpp}

\item
最后,必须存储为一个类型生成的元数据以供重用:

\begin{cpp}
    llvm::DenseMap<TypeDenoter *, llvm::MDNode *> MetadataCache;
    // …
};
\end{cpp}
\end{enumerate}

现在已经定义了构造所需的变量,必须添加创建元数据所需的方法:

\begin{enumerate}
\item
构造函数初始化数据成员:

\begin{cpp}
CGTBAA::CGTBAA(CGModule &CGM)
    : CGM(CGM),
    MDHelper(llvm::MDBuilder(CGM.getLLVMCtx())),
    Root(nullptr) {}
\end{cpp}

\item
必须惰性地实例化类型层次结构的根,将其命名为Simple tinylang TBAA:

\begin{cpp}
llvm::MDNode *CGTBAA::getRoot() {
    if (!Root)
        Root = MDHelper.createTBAARoot("Simple tinylang TBAA");
    return Root;
}
\end{cpp}

\item
对于标量类型,必须在MDBuilder类的帮助下根据类型的名称创建元数据节点。新的元数据节点存储在缓存中:

\begin{cpp}
llvm::MDNode *
CGTBAA::createScalarTypeNode(TypeDeclaration *Ty,
                            StringRef Name,
                            llvm::MDNode *Parent) {
    llvm::MDNode *N =
        MDHelper.createTBAAScalarTypeNode(Name, Parent);
    return MetadataCache[Ty] = N;
}
\end{cpp}

\item
为记录创建元数据的方法更加复杂,所以必须枚举记录的所有字段。与标量类型类似,新的元数据节点存储在缓存中:

\begin{cpp}
llvm::MDNode *CGTBAA::createStructTypeNode(
        TypeDeclaration *Ty, StringRef Name,
        llvm::ArrayRef<std::pair<llvm::MDNode *, uint64_t>> Fields) {
    llvm::MDNode *N =
        MDHelper.createTBAAStructTypeNode(Name, Fields);
    return MetadataCache[Ty] = N;
}
\end{cpp}

\item
要返回tinylang类型的元数据,需要创建类型层次结构。由于tinylang的类型系统非常有限,可以使用一种简单的方法。每个标量类型都是映射到根节点的唯一类型,并且将所有指针映射到单个类型,再结构化类型引用这些节点。若不能映射类型,则返回nullptr:

\begin{cpp}
llvm::MDNode *CGTBAA::getTypeInfo(TypeDeclaration *Ty) {
    if (llvm::MDNode *N = MetadataCache[Ty])
        return N;

    if (auto *Pervasive =
            llvm::dyn_cast<PervasiveTypeDeclaration>(Ty)) {
        StringRef Name = Pervasive->getName();
        return createScalarTypeNode(Pervasive, Name, getRoot());
    }
    if (auto *Pointer =
            llvm::dyn_cast<PointerTypeDeclaration>(Ty)) {
        StringRef Name = "any pointer";
        return createScalarTypeNode(Pointer, Name, getRoot());
    }
    if (auto *Array =
            llvm::dyn_cast<ArrayTypeDeclaration>(Ty)) {
        StringRef Name = Array->getType()->getName();
        return createScalarTypeNode(Array, Name, getRoot());
    }
    if (auto *Record =
            llvm::dyn_cast<RecordTypeDeclaration>(Ty)) {
        llvm::SmallVector<std::pair<llvm::MDNode *, uint64_t>,
        4> Fields;
        auto *Rec =
            llvm::cast<llvm::StructType>(CGM.convertType(Record));
        const llvm::StructLayout *Layout =
        CGM.getModule()->getDataLayout().getStructLayout(Rec);

        unsigned Idx = 0;
        for (const auto &F : Record->getFields()) {
            uint64_t Offset = Layout->getElementOffset(Idx);
            Fields.emplace_back(getTypeInfo(F.getType()), Offset);
            ++Idx;
        }
        StringRef Name = CGM.mangleName(Record);
        return createStructTypeNode(Record, Name, Fields);
    }
    return nullptr;
}
\end{cpp}

\item
获取元数据的一般方法是getAccessTagInfo()。要获取TBAA访问标记信息,必须添加对getTypeInfo()函数的调用。这个函数期望TypeDeclaration作为它的参数,可从生成元数据的指令中检索:

\begin{cpp}
llvm::MDNode *CGTBAA::getAccessTagInfo(TypeDeclaration *Ty) {
    return getTypeInfo(Ty);
}
\end{cpp}

\end{enumerate}

最后,为了生成TBAA元数据,只需要将元数据添加到tinylang中成的所有加载和存储指令上即可。

例如,在CGProcedure:: writvariable()中,存储一个全局变量使用一个存储指令:

\begin{cpp}
    Builder.CreateStore(Val, CGM.getGlobal(D));
\end{cpp}

为了修饰这个特定的指令,需要用以下几行替换这一行,其中decorateInst()将TBAA元数据添加到这个存储指令中:

\begin{cpp}
    auto *Inst = Builder.CreateStore(Val, CGM.getGlobal(D));
    // NOTE: V is of the VariableDeclaration class, and
    // the getType() method in this class retrieves the
    // TypeDeclaration that is needed for decorateInst().
    CGM.decorateInst(Inst, V->getType());
\end{cpp}

有了这些更改,就完成了TBAA元数据的生成。

现在,可以将示例tinylang文件编译为LLVM中间表示,以查看新实现的TBAA元数据。例如,以下文件Person.mod:

\begin{shell}
MODULE Person;

TYPE
    Person = RECORD
                Height: INTEGER;
                Age: INTEGER
             END;

PROCEDURE Set(VAR p: Person);
BEGIN
    p.Age := 18;
END Set;

END Person.
\end{shell}

本章的构建目录中构建的tinylang编译器可以用来生成这个文件的中间表示:

\begin{shell}
$ tools/driver/tinylang -emit-llvm ../examples/Person.mod
\end{shell}

新生成的Person.ll文件中,可以看到存储指令是用本章中生成的TBAA元数据,其中元数据反映了最初声明的记录类型的字段:

\begin{shell}
; ModuleID = '../examples/Person.mod'
source_filename = "../examples/Person.mod"
target datalayout = "e-m:o-i64:64-i128:128-n32:64-S128"
target triple = "arm64-apple-darwin22.6.0"

define void @_t6Person3Set(ptr nocapture dereferenceable(16) %p) {
entry:
    %0 = getelementptr inbounds ptr, ptr %p, i32 0, i32 1
    store i64 18, ptr %0, align 8, !tbaa !0
    ret void
}

!0 = !{!"_t6Person6Person", !1, i64 0, !1, i64 8}
!1 = !{!"INTEGER", !2, i64 0}
!2 = !{!"Simple tinylang TBAA"}
\end{shell}

既然了解了如何生成TBAA元数据,我们将在下一节中探索一个非常相似的主题:生成调试元数据。



























================================================
FILE: content/part2/chapter6/3.tex
================================================

为了允许源代码级调试,必须添加调试信息。LLVM中对调试信息的支持使用调试元数据来描述源语言的类型和其他静态信息,并使用intrinsic来跟踪变量值。LLVM核心库在Unix系统上以DWARF格式生成调试信息,在Windows系统上以PDB格式生成调试信息。我们将在下一节中查看其总体结构。

\mySubsubsection{6.3.1.}{理解调试元数据的一般结构}

为了描述通用结构,LLVM使用的元数据类似于用于基于类型分析的元数据。静态结构描述了文件、编译单元、函数和词法块,以及使用的数据类型。

使用的主要类是llvm::DIBuilder,使用llvm/IR/DIBuilder目录下的头文件来获取类声明。此构建器类提供了一个易于使用的接口来创建调试元数据。之后,元数据将添加到LLVM对象(如全局变量)中,或者在调试内部函数的调用中使用。下面是一些构建器类可以创建的元数据:

\begin{itemize}
\item
llvm::DIFile:使用文件名和包含该文件的目录的绝对路径描述一个文件。可以使用createFile()方法来创建,文件可以包含主编译单元,包含导入的声明。

\item
llvm::DICompileUnit:用于描述当前编译单元。除其他事项外,还指定源语言、特定于编译器的生产者字符串、是否启用优化,当然还有DIFile(编译单元驻留在其中)。可以通过createCompileUnit()来创建。

\item
llvm:: dissubprogram:描述一个函数。这里最重要的信息是作用域(通常是嵌套函数的DICompileUnit或dissubprogram)、函数名、修改后的函数名和函数类型。可以通过调用createFunction()来创建。

\item
llvm::DILexicalBlock:描述了一个词法块,并对许多高级语言中的块范围进行了建模。可以通过调用createLexicalBlock()来创建。
\end{itemize}

LLVM对编译器翻译的语言不做任何假设,所以没有关于该语言的数据类型的信息。要支持源代码级调试,特别是在调试器中显示变量值,还必须添加类型信息。下面是一些重要的构念:

\begin{itemize}
\item
createBasicType()函数返回一个指向llvm::DIBasicType类的指针,创建元数据来描述基本类型,例如tinylang中的INTEGER或C++中的int。除了类型的名称之外,所需的参数还包括以位为单位的大小和编码——例如,若是有符号类型或无符号类型。

\item
有几种方法可以构造复合数据类型的元数据,如llvm::DIComposite类所示。可以使用createArrayType()、createStructType()、createUnionType()和createVectorType()函数分别实例化数组、结构、联合和矢量数据类型的元数据。这些函数需要期望的参数,例如基类型和数组类型的订阅数量或结构类型的字段成员列表。

\item
还有支持枚举、模板、类等的方法。
\end{itemize}

函数列表显示必须将源语言的每个细节添加到调试信息中,假设llvm::DIBuilder类的实例称为DBuilder,还假设在一个名为file的文件中有一些tinylang源文件。/home/llvmuser目录下的File.mod文件中,其第5行是Func():INTEGER函数,第7行包含一个局部VAR i:INTEGER声明。为此创建元数据,从文件的信息开始。这里,需要指定文件名和文件所在文件夹的绝对路径:

\begin{cpp}
llvm::DIFile *DbgFile = DBuilder.createFile("File.mod",
                                            "/home/llvmuser");
\end{cpp}

该文件在tinylang中是一个模块,这使得它成为LLVM的编译单元。这包含了很多信息:

\begin{cpp}
bool IsOptimized = false;
llvm::StringRef CUFlags;
unsigned ObjCRunTimeVersion = 0;
llvm::StringRef SplitName;
llvm::DICompileUnit::DebugEmissionKind EmissionKind =
    llvm::DICompileUnit::DebugEmissionKind::FullDebug;
llvm::DICompileUnit *DbgCU = DBuilder.createCompileUnit(
    llvm::dwarf::DW_LANG_Modula2, DbgFile, "tinylang",
    IsOptimized, CUFlags, ObjCRunTimeVersion, SplitName,
    EmissionKind);
\end{cpp}

此外,调试器需要知道源语言。DWARF标准定义了一个包含所有公共值的枚举,这样做的一个缺点是不能简单地添加新的源语言。要做到这一点,必须在DWARF委员会创建一个请求。请注意,调试器和其他调试工具也需要支持一种新语言——仅仅向枚举中添加一个新成员远远不够。

许多情况下,选择一种接近源语言的语言就足够了。在tinylang的例子中,这是Modula-2,使用DW\_LANG\_Modula2作为语言标识符。编译单元保存在文件中,该文件由我们前面创建的DbgFile变量表示。

此外,调试信息可以携带有关生成器的信息,这些信息可以是编译器的名称和版本信息。这里,只是传递了tinylang字符串。若不想添加这些信息,则可以简单地使用一个空字符串作为参数。

下一组信息包括IsOptimized标志,指示编译器是否开启了优化。通常,这个标志来自于-O命令行开关,可以使用CUFlags参数将其他参数设置传递给调试器。这里没有使用这个,直接传递一个空字符串。我们也不使用Objective-C,所以将0作为Objective-C运行时参数。

通常,调试信息嵌入在我们正在创建的目标文件中。若想要将调试信息写入一个单独的文件,则SplitName参数必须包含这个文件的名称;否则,只需传递一个空字符串就足够了。最后,可以定义应该发出的调试信息的级别。默认值是完整的调试信息,正如FullDebug枚举值的使用所表明的那样,但若只想生成行号,也可以选择LineTablesOnly值,或者根本不生成调试信息的NoDebug值。对于后者,最好一开始就不要创建调试信息。

我们的极简源码只使用INTEGER数据类型,这是一个带符号的32位值。为这种类型创建元数据很简单:

\begin{cpp}
llvm::DIBasicType *DbgIntTy =
    DBuilder.createBasicType("INTEGER", 32,
        llvm::dwarf::DW_ATE_signed);
\end{cpp}

要为函数创建调试元数据,必须首先为签名创建类型,然后为函数本身创建元数据,这类似于为函数创建IR。函数的签名是一个数组,其中所有类型的参数按源顺序排列,函数的返回类型作为索引0处的第一个元素。通常,这个数组是动态构造的。在本例中,还可以静态地构造元数据。这对于内部函数很有用,比如模块初始化。通常,这些函数的参数是已知的,编译器编写器可以硬编码它们:

\begin{cpp}
llvm::Metadata *DbgSigTy = {DbgIntTy};
llvm::DITypeRefArray DbgParamsTy =
            DBuilder.getOrCreateTypeArray(DbgSigTy);
llvm::DISubroutineType *DbgFuncTy =
            DBuilder.createSubroutineType(DbgParamsTy);
\end{cpp}

我们的函数有INTEGER返回类型,没有其他参数,因此DbgSigTy数组只包含指向该类型元数据的指针。该静态数组被转换为类型数组,然后用于创建函数的类型。

函数本身需要更多的数据:

\begin{cpp}
unsigned LineNo = 5;
unsigned ScopeLine = 5;
llvm::DISubprogram *DbgFunc = DBuilder.createFunction(
    DbgCU, "Func", "_t4File4Func", DbgFile, LineNo,
    DbgFuncTy, ScopeLine, llvm::DISubprogram::FlagPrivate,
    llvm::DISubprogram::SPFlagLocalToUnit);
\end{cpp}

函数属于编译单元,在示例中,编译单元存储在DbgCU变量中。需要在源文件中指定函数的名称,即Func,并且修改后的名称存储在目标文件中。这些信息有助于调试器定位函数的机器码。根据tinylang规则,修改后的名称为\_t4File4Func,还必须指定包含函数的文件。

乍一看,这可能令人惊讶,但想想C和C++中的包含机制:函数可以存储在不同的文件中,然后在主编译单元中使用\#include包含该文件。这里的情况并非如此,我们使用与编译单元使用的文件相同的文件。接下来,传递函数的行号和函数类型。函数的行号不能是函数的词法作用域开始的行号,可以指定不同的ScopeLine。函数也有保护,在这里用FlagPrivate值指定它来指示private函数。函数保护的其他可能值是FlagPublic和FlagProtected,分别用于public和protected域内的函数。

除了保护级别,还可以指定其他标志。例如,FlagVirtual表示虚函数,FlagNoReturn表示该函数不返回给调用者。可以在LLVM include文件中找到可能值的完整列表,即llvm/include/llvm/IR/DebugInfoFlags.def。

最后,可以指定特定于函数的标志。最常用的标志是SPFlagLocalToUnit值,表明该函数是编译单元的本地函数。MainSubprogram值也经常使用,表明这个函数是应用程序的主要函数。前面提到的LLVM包含文件,还列出了与特定于函数的标志相关的所有可能值。

目前,我们只创建了引用静态数据的元数据。变量是动态的,因此将在下一节中探讨如何将静态元数据添加到IR代码中以访问变量。

\mySubsubsection{6.3.2.}{跟踪变量及其值}

前一节中描述的类型元数据需要与源程序的变量相关联。对于全局变量,这非常简单。llvm::DIBuilder类的createGlobalVariableExpression()函数创建描述全局变量的元数据。这包括源文件中变量的名称、修改过的名称、源文件等。LLVM IR中的全局变量由GlobalVariable类的实例表示。这个类有一个名为addDebugInfo()的方法,可将从createGlobalVariableExpression()返回的元数据节点与全局变量关联起来。

对于局部变量,需要采取另一种方法。因为只知道值,所以LLVM IR不知道表示局部变量的类。LLVM社区开发的解决方案是将对函数的调用插入到函数的IR代码中。内在函数是LLVM知道的函数,所以可以用它做一些神奇的事情。大多数情况下,内在函数不会导致机器级别的子例程调用。这里,函数调用可将元数据与值关联起来,函数内最重要的调试元数据是llvm.dbg.declare和llvm.dbg.value。

llvm.dbg.declare内部变量提供信息,由前端生成,用于声明局部变量。本质上,this描述了局部变量的地址。优化过程中,通道可以用(可能多次)调用llvm.dbg来替换这个内在的调用。值来保存调试信息并跟踪本地源变量。优化之后,llvm.dbg.declare用于描述局部变量在内存中的程序点,所以可能会出现多次调用。

另一方面,只要将局部变量设置为新值,就会调用llvm.dbg.value。这个内部变量描述的是局部变量的值,而不是它的地址。

这一切是如何运作的呢?LLVM IR表示和通过LLVM::DIBuilder类进行的编程创建略有不同,因此将对两者进行研究。

继续上一节的示例,将使用alloca指令为function函数中的I变量分配本地存储:

\begin{shell}
@i = alloca i32
\end{shell}

之后,必须添加对llvm.dbg.declare的调用:

\begin{shell}
call void @llvm.dbg.declare(metadata ptr %i,
                metadata !1, metadata !DIExpression())
\end{shell}

第一个参数是局部变量的地址。第二个参数是描述局部变量的元数据,通过调用本地变量createAutoVariable()或调用llvm::DIBuilder类参数createParameterVariable()来创建。最后,第三个参数描述一个地址表达式,稍后将对此进行解释。

来实现IR的创建,可以通过调用llvm::IRBuilder<>类的CreateAlloca()方法来为本地@i变量分配存储空间:

\begin{cpp}
llvm::Type *IntTy = llvm::Type::getInt32Ty(LLVMCtx);
llvm::Value *Val = Builder.CreateAlloca(IntTy, nullptr, "i");
\end{cpp}

LLVMCtx变量是使用的上下文类,而Builder是llvm::IRBuilder<>类的使用实例。

局部变量也需要通过元数据来描述:

\begin{cpp}
llvm::DILocalVariable *DbgLocalVar =
    Dbuilder.createAutoVariable(DbgFunc, "i", DbgFile,
                                7, DbgIntTy);
\end{cpp}

使用上一节中的值,可以指定该变量是DbgFunc函数的一部分,称为i,在DbgFile文件的第7行中定义,并且是DbgIntTy类型。

最后,使用llvm.dbg.declare内部变量将调试元数据与变量的地址关联起来。使用llvm::DIBuilder避免添加调用的所有细节:

\begin{cpp}
llvm::DILocation *DbgLoc =
                llvm::DILocation::get(LLVMCtx, 7, 5, DbgFunc);
DBuilder.insertDeclare(Val, DbgLocalVar,
                        DBuilder.createExpression(), DbgLoc,
                        Val.getParent());
\end{cpp}

同样,必须为变量指定一个源位置。llvm::DILocation的实例是一个容器,保存与作用域相关联的位置的行和列。此外,insertDeclare()方法将调用添加到LLVM IR的内部函数。就这个函数的参数而言,需要存储在Val中的变量的地址,以及存储在DbgValVar中的变量的调试元数据。还传递了一个空地址表达式和前面创建的调试位置,与普通指令一样,需要指定将调用插入到哪个基本块中。若指定一个基本块,则在末尾插入调用。或者,可以指定一条指令,然后在该指令之前插入调用。还有一个指向alloca指令的指针,这是插入到底层基本块中的最后一条指令。因此,可以使用这个基本块,并且调用会添加在alloca指令之后。

若局部变量的值发生了变化,必须向IR添加对llvm.dbg.value的调用,以设置局部变量的新值。可以使用llvm::DIBuilder类的insertValue()方法来实现。

当实现函数的IR生成时,使用了一种高级算法(主要使用值),避免为局部变量分配存储。需要添加调试信息,所以使用llvm.dbg.value的频率比在clang生成的IR中看到的要高得多。

若变量没有专用存储空间,而是更大的聚合类型的一部分,该怎么办?可能出现这种情况的一种情况是使用嵌套函数。要实现对调用者堆栈帧的访问,必须收集结构中所有使用的变量,并将指向该记录的指针传递给调用函数。调用的函数内部,可以引用调用方的变量,就好像是函数的局部变量一样。不同的是,这些变量现在是总量的一部分。

对llvm.dbg.declare的调用中,若调试元数据描述了第一个参数所指向的整个内存,则使用空表达式。但若只描述内存的一部分,则需要添加一个表达式,指示元数据应用于内存的哪一部分。

嵌套帧的情况下,需要计算帧中的偏移量。需要访问DataLayout实例,可以从创建IR代码的LLVM模块获得该实例。若llvm::Module实例命名为Mod,保存嵌套帧结构的变量命名为frame,并且是llvm::StructType类型,可以通过以下方式访问帧的第三个成员。这个访问会提供成员的偏移量:

\begin{cpp}
const llvm::DataLayout &DL = Mod->getDataLayout();
uint64_t Ofs = DL.getStructLayout(Frame)->getElementOffset(3);
\end{cpp}

此外,表达式是由一系列操作创建的。要访问帧的第三个成员,调试器需要向基指针添加偏移量。作为一个例子,需要创建一个数组和相关信息:

\begin{cpp}
llvm::SmallVector<int64_t, 2> AddrOps;
AddrOps.push_back(llvm::dwarf::DW_OP_plus_uconst);
AddrOps.push_back(Offset);
\end{cpp}

从这个数组中,可以创建表达式,然后传递给llvm.dbg.declare,而不是空表达式:

\begin{cpp}
llvm::DIExpression *Expr = DBuilder.createExpression(AddrOps);
\end{cpp}

用户并不限于使用这种偏移操作。DWARF知道许多不同的运算符,从而可以创建相当复杂的表达式。可以在LLVM包含文件(llvm/include/llvm/BinaryFormat/Dwarf.def)中找到完整的操作符列表。

此时,可以为变量创建调试信息。为了使调试器能够遵循源代码中的控制流,还需要提供行号信息。这是下一节的主题。

\mySubsubsection{6.3.3.}{添加行号}

调试器允许程序员逐行调试应用程序,所以调试器需要知道哪些机器指令属于源代码中的哪一行,LLVM允许为每条指令添加一个源位置。在前一节中,创建了llvm::DILocation类型的位置信息,调试位置提供的信息不仅仅是行、列和范围。若需要,可以指定该行内联到的作用域。还可以指出此调试位置属于隐式代码——即前端生成,但不在源码中的代码。

此信息可以添加到指令之前,必须将调试位置包装在llvm::DebugLoc对象中。为此,必须简单地将从llvm::DILocation类获得的位置信息传递给llvm::DebugLoc构造函数。通过这种包装,LLVM可以跟踪位置信息。虽然源代码中的位置不会改变,但可以在优化期间删除为源码级语句或表达式生成的机器码,这种封装有助于处理这些变化。

添加行号信息主要归结为从AST检索行号信息,并将其添加到生成的指令中。指令类具有setDebugLoc()方法,该方法将位置信息添加到指令上。

在下一节中,将学习如何生成调试信息,并将其添加到tinylang编译器中。

\mySubsubsection{6.3.4.}{使tinylang支持调试}

我们将调试元数据的生成封装在新的CGDebugInfo类中。此外,将声明放在tinylang/CodeGen/CGDebugInfo.h头文件中,并将定义放在tinylang/CodeGen/CGDebugInfo.cpp文件中。

CGDebugInfo类有五个重要的成员,需要一个对模块代码生成器CGM的引用,所以需要将类型从AST表示转换为LLVM类型。当然,需要一个名为Dbuilder的llvm::DIBuilder类的实例,还需要一个指向编译单元实例的指针,将其存储在成员CU中。

为了避免再次为类型创建调试元数据,必须添加一个映射来缓存此信息,这个成员叫做TypeCache。最后,需要一种方法来管理范围信息,为此必须基于llvm::SmallVector<>类ScopeStack创建一个堆栈:

\begin{cpp}
CGModule &CGM;
llvm::DIBuilder DBuilder;
llvm::DICompileUnit *CU;
llvm::DenseMap<TypeDeclaration *, llvm::DIType *>
    TypeCache;
llvm::SmallVector<llvm::DIScope *, 4> ScopeStack;
\end{cpp}

CGDebugInfo类的以下方法利用了这些成员:

\begin{enumerate}
\item
首先,需要在构造函数中创建编译单元,还在这里创建了包含编译单元的文件。稍后,可以通过CU成员引用该文件。

构造函数的代码如下所示:

\begin{cpp}
CGDebugInfo::CGDebugInfo(CGModule &CGM)
        : CGM(CGM), DBuilder(*CGM.getModule()) {
    llvm::SmallString<128> Path(
        CGM.getASTCtx().getFilename());
    llvm::sys::fs::make_absolute(Path);

    llvm::DIFile *File = DBuilder.createFile(
        llvm::sys::path::filename(Path),
        llvm::sys::path::parent_path(Path));

    bool IsOptimized = false;
    llvm::StringRef CUFlags;
    unsigned ObjCRunTimeVersion = 0;
    llvm::StringRef SplitName;
    llvm::DICompileUnit::DebugEmissionKind EmissionKind =
        llvm::DICompileUnit::DebugEmissionKind::FullDebug;
    CU = DBuilder.createCompileUnit(
        llvm::dwarf::DW_LANG_Modula2, File, "tinylang",
        IsOptimized, CUFlags, ObjCRunTimeVersion,
        SplitName, EmissionKind);
}
\end{cpp}

\item
通常,我们需要提供行号。行号可以从源管理器位置派生,这在大多数AST节点中都是可用的。源代码管理器可以将其转换为行号:

\begin{cpp}
    CGDebugInfo::CGDebugInfo(CGModule &CGM)
unsigned CGDebugInfo::getLineNumber(SMLoc Loc) {
    return CGM.getASTCtx().getSourceMgr().FindLineNumber(
        Loc);
}
\end{cpp}

\item
关于作用域的信息保存在堆栈中,需要打开和关闭作用域以及检索当前作用域的方法。编译单元是全局作用域,则会自动添加:

\begin{cpp}
llvm::DIScope *CGDebugInfo::getScope() {
    if (ScopeStack.empty())
    openScope(CU->getFile());
    return ScopeStack.back();
}

void CGDebugInfo::openScope(llvm::DIScope *Scope) {
    ScopeStack.push_back(Scope);
}

void CGDebugInfo::closeScope() {
    ScopeStack.pop_back();
}
\end{cpp}

\item
接下来,必须为需要转换的类型的每个类别创建一个方法。getPervasiveType()方法为基本类型创建调试元数据。注意encoding参数的使用,将INTEGER类型声明为有符号类型,并将BOOLEAN类型编码为布尔值:

\begin{cpp}
llvm::DIType *
CGDebugInfo::getPervasiveType(TypeDeclaration *Ty) {
    if (Ty->getName() == "INTEGER") {
        return DBuilder.createBasicType(
            Ty->getName(), 64, llvm::dwarf::DW_ATE_signed);
    }
    if (Ty->getName() == "BOOLEAN") {
        return DBuilder.createBasicType(
            Ty->getName(), 1, llvm::dwarf::DW_ATE_boolean);
    }
    llvm::report_fatal_error(
        "Unsupported pervasive type");
}
\end{cpp}

\item
若只是重命名类型名,必须将其映射到类型定义,需要利用作用域和行号信息:

\begin{cpp}
llvm::DIType *
CGDebugInfo::getAliasType(AliasTypeDeclaration *Ty) {
    return DBuilder.createTypedef(
        getType(Ty->getType()), Ty->getName(),
        CU->getFile(), getLineNumber(Ty->getLocation()),
        getScope());
}
\end{cpp}

\item
为数组创建调试信息需要指定数组大小和对齐方式,可以从DataLayout类中检索这些数据,还需要指定数组的索引范围:

\begin{cpp}
llvm::DIType *
CGDebugInfo::getArrayType(ArrayTypeDeclaration *Ty) {
    auto *ATy =
        llvm::cast<llvm::ArrayType>(CGM.convertType(Ty));
    const llvm::DataLayout &DL =
        CGM.getModule()->getDataLayout();

    Expr *Nums = Ty->getNums();
    uint64_t NumElements =
        llvm::cast<IntegerLiteral>(Nums)
            ->getValue()
            .getZExtValue();
    llvm::SmallVector<llvm::Metadata *, 4> Subscripts;
    Subscripts.push_back(
        DBuilder.getOrCreateSubrange(0, NumElements));
    return DBuilder.createArrayType(
        DL.getTypeSizeInBits(ATy) * 8,
        1 << Log2(DL.getABITypeAlign(ATy)),
        getType(Ty->getType()),
        DBuilder.getOrCreateArray(Subscripts));
}
\end{cpp}

\item
使用所有这些单一方法,创建一个中心方法来为类型创建元数据,这些元数据还负责缓存数据:

\begin{cpp}
llvm::DIType *
CGDebugInfo::getType(TypeDeclaration *Ty) {
    if (llvm::DIType *T = TypeCache[Ty])
        return T;
    if (llvm::isa<PervasiveTypeDeclaration>(Ty))
        return TypeCache[Ty] = getPervasiveType(Ty);
    else if (auto *AliasTy =
        llvm::dyn_cast<AliasTypeDeclaration>(Ty))
            return TypeCache[Ty] = getAliasType(AliasTy);
    else if (auto *ArrayTy =
        llvm::dyn_cast<ArrayTypeDeclaration>(Ty))
            return TypeCache[Ty] = getArrayType(ArrayTy);
    else if (auto *RecordTy =
        llvm ::dyn_cast<RecordTypeDeclaration>(Ty))
        r   eturn TypeCache[Ty] = getRecordType(RecordTy);
    llvm::report_fatal_error("Unsupported type");
    return nullptr;
}
\end{cpp}

\item
还需要添加一个方法来为全局变量生成元数据:

\begin{cpp}
void CGDebugInfo::emitGlobalVariable(
VariableDeclaration *Decl,
llvm::GlobalVariable *V) {
    llvm::DIGlobalVariableExpression *GV =
        DBuilder.createGlobalVariableExpression(
            getScope(), Decl->getName(), V->getName(),
            CU->getFile(),
            getLineNumber(Decl->getLocation()),
            getType(Decl->getType()), false);
    V->addDebugInfo(GV);
}
\end{cpp}

\item
要为过程发送调试信息,需要为过程类型创建元数据。为此,需要一个参数类型的列表,第一个条目的返回类型。若过程没有返回类型,则必须使用未指定类型;这称为void,与C语言类似。若形参是引用,则需要添加引用类型;否则,必须将类型添加到列表中:

\begin{cpp}
llvm::DISubroutineType *
CGDebugInfo::getType(ProcedureDeclaration *P) {
    llvm::SmallVector<llvm::Metadata *, 4> Types;
    const llvm::DataLayout &DL =
        CGM.getModule()->getDataLayout();
    // Return type at index 0
    if (P->getRetType())
        Types.push_back(getType(P->getRetType()));
    else
        Types.push_back(
            DBuilder.createUnspecifiedType("void"));
    for (const auto *FP : P->getFormalParams()) {
        llvm::DIType *PT = getType(FP->getType());
        if (FP->isVar()) {
            llvm::Type *PTy = CGM.convertType(FP->getType());
            PT = DBuilder.createReferenceType(
                llvm::dwarf::DW_TAG_reference_type, PT,
                DL.getTypeSizeInBits(PTy) * 8,
                1 << Log2(DL.getABITypeAlign(PTy)));
        }
        Types.push_back(PT);
    }
    return DBuilder.createSubroutineType(
        DBuilder.getOrCreateTypeArray(Types));
}
\end{cpp}

\item
对于过程本身,现在可以使用在前一步中创建的过程类型创建调试信息。过程也会打开一个新的作用域,所以必须将过程压入作用域堆栈,还要将LLVM函数对象与新的调试信息关联起来:

\begin{cpp}
void CGDebugInfo::emitProcedure(
        ProcedureDeclaration *Decl, llvm::Function *Fn) {
    llvm::DISubroutineType *SubT = getType(Decl);
    llvm::DISubprogram *Sub = DBuilder.createFunction(
        getScope(), Decl->getName(), Fn->getName(),
        CU->getFile(), getLineNumber(Decl->getLocation()),
        SubT, getLineNumber(Decl->getLocation()),
        llvm::DINode::FlagPrototyped,
        llvm::DISubprogram::SPFlagDefinition);
    openScope(Sub);
    Fn->setSubprogram(Sub);
}
\end{cpp}

\item
当到达过程的末尾时,必须通知构建器完成该过程的调试信息的构造,还需要从作用域堆栈中删除这个过程:

\begin{cpp}
void CGDebugInfo::emitProcedureEnd(
ProcedureDeclaration *Decl, llvm::Function *Fn) {
    if (Fn && Fn->getSubprogram())
        DBuilder.finalizeSubprogram(Fn->getSubprogram());
    closeScope();
}
\end{cpp}

\item
最后,当添加完调试信息后,需要在构建器上实现finalize()方法,再验证生成的调试信息。这是开发过程中的一个重要步骤,可以帮助找到错生成的元数据:

\begin{cpp}
void CGDebugInfo::finalize() { DBuilder.finalize(); }
\end{cpp}

\end{enumerate}

调试信息应该只在用户请求时生成,所以需要一个新的命令行开关。我们将把它添加到CGModule类的文件中,也将在这个类中使用它:

\begin{cpp}
static llvm::cl::opt<bool>
    Debug("g", llvm::cl::desc("Generate debug information"),
        llvm::cl::init(false));
\end{cpp}

-g选项可以与tinylang编译器一起使用,以生成调试元数据。

此外,CGModule类持有std::unique\_ptr<CGDebugInfo>类的实例。指针在用于设置命令行开关的构造函数中初始化:

\begin{cpp}
    if (Debug)
        DebugInfo.reset(new CGDebugInfo(*this));
\end{cpp}

在CGModule.h中定义的getter方法中,我们只返回指针:

\begin{cpp}
CGDebugInfo *getDbgInfo() {
    return DebugInfo.get();
}
\end{cpp}

生成调试元数据的常用模式是检索指针,并检查它是否有效。例如,在创建一个全局变量之后,可以像这样添加调试信息:

\begin{cpp}
VariableDeclaration *Var = …;
llvm::GlobalVariable *V = …;
if (CGDebugInfo *Dbg = getDbgInfo())
    Dbg->emitGlobalVariable(Var, V);
\end{cpp}

为了添加行号信息,需要在CGDebugInfo类中使用一个名为getDebugLoc()的转换方法,将AST中的位置信息转换为调试元数据:

\begin{cpp}
llvm::DebugLoc CGDebugInfo::getDebugLoc(SMLoc Loc) {
    std::pair<unsigned, unsigned> LineAndCol =
        CGM.getASTCtx().getSourceMgr().getLineAndColumn(Loc);
    llvm::DILocation *DILoc = llvm::DILocation::get(
        CGM.getLLVMCtx(), LineAndCol.first, LineAndCol.second,
        getScope());
    return llvm::DebugLoc(DILoc);
}
\end{cpp}

此外,可以调用CGModule类中的实用函数来将行号信息添加到指令中:

\begin{cpp}
void CGModule::applyLocation(llvm::Instruction *Inst,
                             llvm::SMLoc Loc) {
    if (CGDebugInfo *Dbg = getDbgInfo())
        Inst->setDebugLoc(Dbg->getDebugLoc(Loc));
}
\end{cpp}

通过这种方式,可以添加编译器的调试信息。







































================================================
FILE: content/part2/chapter6/4.tex
================================================
本章中,了解了如何在LLVM中抛出和捕获异常,以及如何生成IR来利用此功能。为了增强IR的范围,了解了如何将各种元数据添加到指令。用于基于类型的别名分析的元数据为LLVM优化器提供信息,并帮助进行某些优化以生成更好的机器代码。用户总是喜欢使用源代码级调试器,并且通过向IR代码中添加调试信息,可以实现编译器的这一重要特性。

优化IR代码是LLVM的核心任务。下一章中,我们将学习通道管理器是如何工作的,以及如何影响通道管理器所管理的优化流水线。

================================================
FILE: content/part2/chapter7/0.tex
================================================

LLVM使用一系列的通道来优化IR,一个通道在IR的一个单元上运行,例如一个函数或一个模块。操作可以是转换,以定义的方式更改IR,也可以是分析,其收集诸如依赖关系之类的信息。这一系列的通道称为通道流水线,通道管理器在编译器生成的IR上执行通道流水线,所以需要了解通道管理器的作用以及如何构造一个通道流水线。编程语言的语义可能需要开发新的通道,我们必须将这些通道添加到流水线中。

本章中,将了解以下内容:

\begin{itemize}
\item
如何利用LLVM通道管理器在LLVM内实现通道

\item
如何在LLVM项目中实现一个instrumentation通道,以及一个单独的插件

\item
使用LLVM工具的ppprofiler通道时,如何使用opt和clang的通道插件

\item
向编译器中添加优化流水时,将使用基于新通道管理器的优化流水扩展tinylang编译器
\end{itemize}

本章结束时,将了解如何开发新的通道,以及如何将其添加到通道流水线中,还可以在编译器中设置通道流水线。

================================================
FILE: content/part2/chapter7/1.tex
================================================

本章使用的代码在这里\url{https://github.com/PacktPublishing/Learn-LLVM-17/tree/main/Chapter07}。

================================================
FILE: content/part2/chapter7/2.tex
================================================
LLVM核心库优化编译器创建的IR并将其转换为目标代码。这项艰巨的任务需要分解成一个个独立的步骤,称为“通道”(passes)。这些通道需要以正确的顺序执行,这是通道管理器的目标。

为什么不硬编码通道的顺序呢?编译器的用户通常希望编译器提供不同级别的优化。开发人员更喜欢快速的编译速度,而不是在开发期间进行优化。最终的应用程序应该运行得尽可能快,并且编译器应该能够执行复杂的优化,并且可以接受较长的编译时间。不同级别的优化需要执行不同数量的优化,所以编译器作者可能希望提供自己的通道,以利用自己对语言的了解。例如,可能希望用内联IR甚至是预先计算的结果,来替换众所周知的库函数。对于C语言,这样的通道是LLVM库的一部分,但对于其他语言,需要自行提供。了解了通道后,可能需要重新定制或添加一些通道。例如,若知道通行证的操作使一些IR代码无法访问,则希望在通道之后运行固定代码删除通道,通道管理器可以协助完成这些需求。

通道通常根据其工作的范围进行分类:

\begin{itemize}
\item
模块传递将整个模块作为输入。这样的传递在给定的模块上执行其工作,并可用于该模块内的过程内操作。

\item
调用图通道对调用图的强连接组件(scc)进行操作,自下而上的顺序遍历组件。

\item
函数通道将单个函数作为输入,并仅在该函数上执行其工作。

\item
循环通道作用于函数内部的循环。
\end{itemize}

除了IR代码之外,通道还可能需要、更新或使某些分析结果无效。执行了许多不同的分析,例如,别名分析或支配树的构造。若一个通道需要这样的分析,则可以向分析管理器请求。若已经计算了信息,将返回缓存的结果;否则,将计算该信息。若一个通道更改了IR代码,则需要宣布保留哪些分析结果,以便可以在必要时使缓存的分析信息无效。

在底层,通道管理器有以下作用:

\begin{itemize}
\item
分析结果在各通道之间共享,这需要跟踪哪个通道,需要哪个分析,以及每个分析的状态。目标是避免不必要的分析预计算,并尽快释放分析结果占用的内存。

\item
这些通道以流水线方式执行。例如,若几个函数通道应该依次执行,通道管理器将在第一个函数上运行这些函数中的每个函数,再将运行第二个函数通道的所有函数,以此类推。这里的基本思想是改进缓存行为,因为编译器只对有限的一组数据(一个IR函数)执行转换,然后切换到下一个有限的数据集。
\end{itemize}

让我们实现一个新的IR转换通道,并探索如何将其添加到优化流水线中。


















================================================
FILE: content/part2/chapter7/3.tex
================================================

一个通道可以在LLVM IR上执行任意复杂度的转换。为了说明添加新通道的机制,我们添加一个执行简单检测的通道。

为了研究程序的性能,了解函数被调用的频率和运行的时间是很有趣的。收集这些数据的一种方法是在每个函数中插入计数器,这个过程称为插装。我们将编写一个简单的插装通道,在每个函数的入口和每个退出点插入一个特殊的函数调用,这些函数收集计时信息并将其写入文件。因此,可以创建一个非常基本的分析器,将其命名为“穷人”(poor people)分析器,或者简而言之——ppprofiler。我们将开发新通道,以便其可以作为一个独立的插件使用,或者作为一个插件添加到LLVM源代码树中。最后,我们将了解如何将LLVM自带的通道集成到框架中。

\mySubsubsection{7.3.1.}{将ppprofiler作为插件进行开发}

本节中,将了解如何在LLVM树中创建一个新通道作为插件。新通道的目标是在函数的入口处插入对\_\_ppp\_enter()函数的调用,并在每个返回指令之前插入对\_\_ppp\_exit()函数的调用。只有当前函数的名称作为参数传递,这些函数的实现可以计算调用的次数,并测量所使用的时间。我们将在本章的最后实现这个运行时支持,并将研究如何开发通道。

我们将源代码存储在ppprofile.cpp文件中。遵循以下步骤:

\begin{enumerate}
\item
首先,包含一些文件:

\begin{cpp}
#include "llvm/ADT/Statistic.h"
#include "llvm/IR/Function.h"
#include "llvm/IR/PassManager.h"
#include "llvm/Passes/PassBuilder.h"
#include "llvm/Passes/PassPlugin.h"
#include "llvm/Support/Debug.h"
\end{cpp}

\item
为了缩短源代码,告诉编译器正在使用llvm命名空间:

\begin{cpp}
using namespace llvm;
\end{cpp}

\item
LLVM的内置调试基础结构要求定义一个调试类型,它是一个字符串。这个字符串随后显示在打印的统计信息中:

\begin{cpp}
#define DEBUG_TYPE "ppprofiler"
\end{cpp}

\item
接下来,将使用ALWAYS\_ENABLED\_STATISTIC宏定义一个计数器变量。第一个参数是计数器变量的名称,而第二个参数是将在统计中打印的文本:

\begin{cpp}
ALWAYS_ENABLED_STATISTIC(
    NumOfFunc, "Number of instrumented functions.");
\end{cpp}

\begin{myNotic}{Note}
可以使用两个宏来定义计数器变量。若使用STATISTIC宏,则只有在启用断言的情况下,或者在CMake命令行中将LLVM\_FORCE\_ENABLE\_STATS设置为ON时,才会在调试构建中收集统计值。若使用ALWAYS\_ENABLED\_STATISTIC宏,则总是收集统计值。但使用-stats命令行选项输出统计信息只适用于前两种方法。若需要,可以通过调用llvm::PrintStatistics(llvm::raw\_ostream)函数打印收集的统计信息。
\end{myNotic}

\item
接下来,必须在匿名命名空间中声明通道类,这个类继承自PassInfoMixin模板。这个模板只添加了一些样板代码,比如name()方法,不用于确定传递的类型,run()方法在执行传递时由LLVM调用,还需要一个名为instrument()的辅助方法:

\begin{cpp}
namespace {
class PPProfilerIRPass
: public llvm::PassInfoMixin<PPProfilerIRPass> {
public:
    llvm::PreservedAnalyses
    run(llvm::Module &M, llvm::ModuleAnalysisManager &AM);

private:
    void instrument(llvm::Function &F,
                    llvm::Function *EnterFn,
                    llvm::Function *ExitFn);
};
}
\end{cpp}

\item
现在,来定义如何插装函数。除了要检测的函数外,还要通道要调用的函数:

\begin{cpp}
void PPProfilerIRPass::instrument(llvm::Function &F,
                                  Function *EnterFn,
                                  Function *ExitFn) {
\end{cpp}

\item
在函数内部,更新统计计数器:

\begin{cpp}
    ++NumOfFunc;
Download .txt
gitextract_ejqkru__/

├── .gitignore
├── LICENSE
├── Learn-LLVM-17.tex
├── README.md
└── content/
    ├── chapter0/
    │   ├── 0.tex
    │   ├── 1.tex
    │   ├── 2.tex
    │   └── 3.tex
    ├── part1/
    │   ├── chapter1/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   ├── 4.tex
    │   │   └── 5.tex
    │   ├── chapter2/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   ├── 4.tex
    │   │   ├── 5.tex
    │   │   ├── 6.tex
    │   │   └── 7.tex
    │   └── part1.tex
    ├── part2/
    │   ├── chapter3/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   ├── 4.tex
    │   │   ├── 5.tex
    │   │   ├── 6.tex
    │   │   ├── 7.tex
    │   │   └── 8.tex
    │   ├── chapter4/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   └── 4.tex
    │   ├── chapter5/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   ├── 4.tex
    │   │   └── 5.tex
    │   ├── chapter6/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   └── 4.tex
    │   ├── chapter7/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   ├── 4.tex
    │   │   ├── 5.tex
    │   │   └── 6.tex
    │   └── part2.tex
    ├── part3/
    │   ├── chapter10/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   ├── 4.tex
    │   │   ├── 5.tex
    │   │   ├── 6.tex
    │   │   └── 7.tex
    │   ├── chapter8/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   ├── 4.tex
    │   │   ├── 5.tex
    │   │   └── 6.tex
    │   ├── chapter9/
    │   │   ├── 0.tex
    │   │   ├── 1.tex
    │   │   ├── 2.tex
    │   │   ├── 3.tex
    │   │   ├── 4.tex
    │   │   ├── 5.tex
    │   │   └── 6.tex
    │   └── part3.tex
    └── part4/
        ├── chapter11/
        │   ├── 0.tex
        │   ├── 1.tex
        │   ├── 2.tex
        │   ├── 3.tex
        │   ├── 4.tex
        │   ├── 5.tex
        │   ├── 6.tex
        │   ├── 7.tex
        │   └── 8.tex
        ├── chapter12/
        │   ├── 0.tex
        │   ├── 1.tex
        │   ├── 2.tex
        │   ├── 3.tex
        │   ├── 4.tex
        │   ├── 5.tex
        │   ├── 6.tex
        │   ├── 7.tex
        │   ├── 8.tex
        │   └── 9.tex
        ├── chapter13/
        │   ├── 0.tex
        │   ├── 1.tex
        │   ├── 2.tex
        │   ├── 3.tex
        │   └── 4.tex
        └── part4.tex
Condensed preview — 104 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (698K chars).
[
  {
    "path": ".gitignore",
    "chars": 84,
    "preview": "*.pdf\n*.aux\n*.log\n*.out\n*.gz\n*toc\n*.listing\n*.synctex(busy)\n/_minted-Learn-LLVM-17/\n"
  },
  {
    "path": "LICENSE",
    "chars": 11357,
    "preview": "                                 Apache License\n                           Version 2.0, January 2004\n                   "
  },
  {
    "path": "Learn-LLVM-17.tex",
    "chars": 13097,
    "preview": "\n\\documentclass[11pt,a4paper,UTF8]{book}\n\n\\usepackage{minted}\n\\usepackage[T1]{fontenc}\n\\usepackage[utf8]{inputenc}\n\\usep"
  },
  {
    "path": "README.md",
    "chars": 1512,
    "preview": "# Learn LLVM 17\nA beginner's guide to learning LLVM compiler tools and core libraries with C++ \n\n(*使用C++学习LLVM编译器和核心库的初学"
  },
  {
    "path": "content/chapter0/0.tex",
    "chars": 454,
    "preview": "\\begin{center}\n\n写一本书需要很多时间和精力。没有我的妻子坦尼娅和女儿波琳娜的支持和理解,这本书是不可能完成的。谢谢你们一直鼓励我!\n\n由于一些个人的挑战,这个项目处于危险之中,很感谢艾米作为作者加入这个项目。若没有她,这本书"
  },
  {
    "path": "content/chapter0/1.tex",
    "chars": 514,
    "preview": "\\textbf{Kai Nacke}是一名专业的IT架构师,目前居住在加拿大多伦多,拥有德国多特蒙德技术大学(Technical University of Dortmund)计算机科学文凭。他关于通用散列函数的毕业论文是当时最好的论文之一"
  },
  {
    "path": "content/chapter0/2.tex",
    "chars": 265,
    "preview": "\\textbf{Akash Kothari}是伊利诺伊LLVM编译器研究实验室的研究助理。他在伊利诺伊大学厄巴纳-香槟分校获得计算机科学博士学位。Akash专注于性能工程、程序合成、形式语义和验证,他还对探索计算和编程系统的历史非常感兴趣。"
  },
  {
    "path": "content/chapter0/3.tex",
    "chars": 3865,
    "preview": "构造编译器是一项复杂而迷人的任务。LLVM项目为编译器提供了可重用的组件,LLVM核心库实现了世界级的优化代码生成器,可以为所有主流CPU架构翻译与源语言无关的机器码中间表示,许多编程语言的编译器已经在使用LLVM。\n\n本书将介绍如何实现自"
  },
  {
    "path": "content/part1/chapter1/0.tex",
    "chars": 291,
    "preview": "为了学习如何使用LLVM,最好先从源代码开始编译LLVM。LLVM是一个伞形项目,GitHub库包含属于LLVM的所有项目的源代码。每个LLVM项目都位于存储库的顶级目录中。除了克隆库之外,本地环境还必须安装构建系统所需的所有工具。\n\n本章"
  },
  {
    "path": "content/part1/chapter1/1.tex",
    "chars": 402,
    "preview": "可以使用各种源来安装LLVM二进制文件。若使用的是Linux,则其发行版包含LLVM库。为什么要自己编译LLVM呢?\n\n首先,并非所有安装包都包含使用LLVM进行开发所需的所有文件,自己编译和安装LLVM可以避免这个问题。另一个原因是,LL"
  },
  {
    "path": "content/part1/chapter1/2.tex",
    "chars": 3190,
    "preview": "\n要使用LLVM,开发系统应该运行一个通用的操作系统,如Linux、FreeBSD、macOS或Windows。可以在不同的模式下构建LLVM和clang,启用调试符号的构建最多需要30 GB的空间。所需的磁盘空间在很大程度上取决于所选择的"
  },
  {
    "path": "content/part1/chapter1/3.tex",
    "chars": 4657,
    "preview": "\n准备好构建工具后,可以从GitHub中下载LLVM项目,并构建LLVM。这一过程在所有平台上都是相同的:\n\n\\begin{enumerate}\n\\item\n配置Git\n\n\\item\n克隆库\n\n\\item\n创建构建目录\n\n\\item\n生成构"
  },
  {
    "path": "content/part1/chapter1/4.tex",
    "chars": 7431,
    "preview": "\nCMake系统使用CMakeLists.txt文件中的项目描述。顶层文件位于llvm目录“llvm/CMakeLists.txt”。其他目录也有CMakeLists.txt文件,这些文件在生成过程中递归包含。\n\n根据项目描述中提供的信息,"
  },
  {
    "path": "content/part1/chapter1/5.tex",
    "chars": 200,
    "preview": "本章中,准备了开发机器来编译LLVM,克隆了GitHub库,并编译了LLVM和clang版本。构建过程可以用CMake变量自定义,了解了有用的变量以及如何更改它们。有了这些知识,就可以根据自己的需要调整LLVM了。\n\n下一节中,我们将进一步"
  },
  {
    "path": "content/part1/chapter2/0.tex",
    "chars": 382,
    "preview": "编译器技术是计算机科学中一个研究领域,其任务是将源语言翻译成机器码。通常,此任务分为三个部分:前端、中端和后端。前端主要处理源语言,而中端执行转换并改进代码,后端负责生成机器码。由于LLVM核心库提供了中端和后端,我们将在本章中专注于前端的"
  },
  {
    "path": "content/part1/chapter2/1.tex",
    "chars": 518,
    "preview": "自从有了计算机,编程语言就开发了数千种。事实证明,所有编译器都必须解决相同的任务,并且编译器的实现最好根据这些任务进行结构化。总得来说,有三个组成部分。前端将源代码转换为中间表示(IR);中端在IR上执行转换,其目标是提高性能或减少代码的大"
  },
  {
    "path": "content/part1/chapter2/2.tex",
    "chars": 1253,
    "preview": "\n算术表达式是每一种编程语言的一部分。下面是一个算术表达式计算语言calc的例子,calc表达式会编译成一个计算以下表达式的应用程序:\n\n\\begin{shell}\nwith a, b: a * (4 + b)\n\\end{shell}\n\n表"
  },
  {
    "path": "content/part1/chapter2/3.tex",
    "chars": 5112,
    "preview": "正如在前一节的示例,编程语言由许多元素组成,如关键字、标识符、数字、操作符等。词法分析器的任务是获取文本输入并从中创建一个标记序列。calc语言由,:,+,-,*,/,(,)和正则表达式([a- za -z])+(标识符)和([0-9])+"
  },
  {
    "path": "content/part1/chapter2/4.tex",
    "chars": 9065,
    "preview": "语法分析由解析器完成,接下来我们将实现解析器。它的基础是前面小节中的语法和词法分析器。解析过程的结果是一个动态数据结构,称为抽象语法树(AST)。AST是输入的一个非常紧凑的表示,非常适合语义分析。\n\n首先,将实现解析器,再了解AST的解析"
  },
  {
    "path": "content/part1/chapter2/5.tex",
    "chars": 2241,
    "preview": "语义分析器遍历AST并检查语言的各种语义规则,例如:变量必须在使用前声明,或者变量的类型必须在表达式中兼容。若语义分析器发现可以改进的情况,还可以输出警告。对于示例表达式语言,语义分析器必须检查是否声明了每个使用的变量(这是该语言所要求的)"
  },
  {
    "path": "content/part1/chapter2/6.tex",
    "chars": 10718,
    "preview": "\n后端的任务是从模块的LLVM IR中创建优化的机器码。IR是后端接口,可以使用C++接口或以文本形式创建,IR也是从AST生成的。\n\n\\mySubsubsection{2.6.1.}{LLVM IR的文本表示}\n\n\\begin{enume"
  },
  {
    "path": "content/part1/chapter2/7.tex",
    "chars": 171,
    "preview": "在本章中,了解了编译器的典型组件,使用算术表达式语言介绍编程语言的语法。了解了如何为这种语言开发前端的典型组件:词法分析器、解析器、语义分析器和代码生成器。代码生成器只生成LLVM IR,并使用LLVM llc静态编译器从中创建目标文件。现"
  },
  {
    "path": "content/part1/part1.tex",
    "chars": 145,
    "preview": "\n将了解如何编译LLVM,并根据需要定制构建;了解LLVM项目是如何组织的,将使用LLVM创建一个项目。最后,将探索编译器的总体结构,并创建一个小型编译器。\n\n\\begin{itemize}\n\\item\n第1章,安装LLVM\n\n\\item\n"
  },
  {
    "path": "content/part2/chapter3/0.tex",
    "chars": 471,
    "preview": "正如在前一章了解到的,编译器通常分为两部分——前端和后端。本章中,将实现一种编程语言的前端——主要处理源语言的部分。我们将学习现实世界的编译器使用的技术,并将其应用到我们的编程语言中。\n\n旅程将从定义编程语言的语法开始,并以抽象语法树(AS"
  },
  {
    "path": "content/part2/chapter3/1.tex",
    "chars": 3289,
    "preview": "与前一章中简单的calc语言相比,真正的编程面临更多挑战。为了了解其中的细节,我们将在本章和后续章节中使用Modula-2的一小部分。Modula-2设计良好,可选支持泛型和面向对象编程(OOP)。但在本书中,我们并不打算创建一个完整的Mo"
  },
  {
    "path": "content/part2/chapter3/2.tex",
    "chars": 388,
    "preview": "tinylang的目录结构遵循我们在第1章安装LLVM中列出的方式。每个组件的源代码位于lib目录的子目录中,头文件位于include/tinylang的子目录中。子目录以组件命名。在第1章安装LLVM中,我们只创建了基本组件。\n\n前一章中"
  },
  {
    "path": "content/part2/chapter3/3.tex",
    "chars": 455,
    "preview": "真正的编译器必须处理许多文件,开发人员用主编译单元的名称来调用编译器。这个编译单元可以引用其他文件——例如,通过C语言中的\\#include指令或Python或Modula-2中的import语句。导入的模块可以导入其他模块,以此类推。所有"
  },
  {
    "path": "content/part2/chapter3/4.tex",
    "chars": 3235,
    "preview": "我们目前还缺少消息的集中定义。在大型软件(如编译器)中,你应该不会希望消息字符串散布各处。若有修改消息或将其翻译成另一种语言的请求,最好将它们放在一个中心位置!一种简单的方法是,每个消息都有一个ID(枚举成员)、一个严重级别(如Error或"
  },
  {
    "path": "content/part2/chapter3/5.tex",
    "chars": 3545,
    "preview": "正如从前一章所知,我们需要一个Token类和一个Lexer类,所以需要TokenKind枚举来为每个Token类提供唯一的编号。拥有一个集所有功能于一体的头文件和实现文件并不能扩展,TokenKind可以普遍使用,并放置在Basic组件中。"
  },
  {
    "path": "content/part2/chapter3/6.tex",
    "chars": 5759,
    "preview": "如前一章所示,解析器是从语法派生出来的。回顾一下所有的构造规则,对于语法的每个规则,创建一个以规则左侧的非终结符命名的方法来解析规则的右侧。根据右边的定义,可以有以下操作:\n\n\\begin{itemize}\n\\item\n对于每个非终结符,调"
  },
  {
    "path": "content/part2/chapter3/7.tex",
    "chars": 12009,
    "preview": "\n我们在上一节中构建的解析器只检查输入的语法,下一步是添加执行语义分析的能力。上一章的calc示例中,解析器构建了一个AST。在另一个单独的阶段中,语义分析器在这个树上工作。这种方法总是可以使用的。本节中,我们将使用一种稍微不同的方法,并将"
  },
  {
    "path": "content/part2/chapter3/8.tex",
    "chars": 243,
    "preview": "本章中,了解了实际编译器在前端使用的技术。从项目布局开始,为词法分析器、解析器和语义分析器创建了单独的库。为了向用户输出消息,扩展了一个现有的LLVM类,允许集中存储消息,词法分析器现在已经分成几个接口。\n\n然后,了解了如何根据语法描述构造"
  },
  {
    "path": "content/part2/chapter4/0.tex",
    "chars": 342,
    "preview": "\n为编程语言创建了修饰抽象语法树(AST)之后,下一个任务是从中生成LLVM IR代码。LLVM IR代码类似于具有人类可读表示的三地址码,所以需要一种系统的方法来将语言概念(如控制结构)翻译成低层LLVM IR。\n\n本章中,将了解LLVM"
  },
  {
    "path": "content/part2/chapter4/1.tex",
    "chars": 7603,
    "preview": "\nLLVM代码生成器将LLVM IR中的一个模块作为输入,并将其转换为目标代码或汇编文本,需要将AST表示转换为IR。为了实现一个IR代码生成器,首先看一个简单的例子,然后开发代码生成器所需的类。完整的实现将分为三类:\n\n\\begin{it"
  },
  {
    "path": "content/part2/chapter4/2.tex",
    "chars": 12850,
    "preview": "\n为了使用AST生成SSA形式的IR代码,可以使用一种称为AST编号的方法。基本思想是,对于每个基本块,我们存储写入该基本块中局部变量的当前值。\n\n\\begin{myNotic}{Note}\n实现基于Braun et al.的论文《Simp"
  },
  {
    "path": "content/part2/chapter4/3.tex",
    "chars": 7933,
    "preview": "\n我们在LLVM模块中收集编译单元的所有函数和全局变量。为了简化IR生成过程,可以将前几节中的所有函数包装到代码生成器类中。为了获得一个工作的编译器,还需要定义想要为其生成代码的目标架构,并添加生成代码的通道。我们将在本章和接下来的几章中实"
  },
  {
    "path": "content/part2/chapter4/4.tex",
    "chars": 237,
    "preview": "本章中,了解了如何实现LLVM IR代码的代码生成器,基本块是保存所有指令和表示分支的重要数据结构。了解了如何为源语言的控制语句创建基本块,以及如何向基本块中添加指令。使用一种现代算法来处理函数中的局部变量,从而减少了IR代码。编译器的目标"
  },
  {
    "path": "content/part2/chapter5/0.tex",
    "chars": 411,
    "preview": "\n现在的高级语言通常使用聚合数据类型和面向对象编程(OOP)结构。LLVM IR对聚合数据类型有一定的支持,而OOP结构(如类)必须自己实现。添加聚合类型会引起传递聚合类型参数的问题。不同的平台有不同的规则(这也会体现在IR上),遵循调用约"
  },
  {
    "path": "content/part2/chapter5/1.tex",
    "chars": 87,
    "preview": "本章使用的代码在这里,\\url{https://github.com/PacktPublishing/ Learn-LLVM-17/tree/main/Chapter05}。"
  },
  {
    "path": "content/part2/chapter5/2.tex",
    "chars": 4442,
    "preview": "对于应用程序来说,只有INTEGER这样的基本类型是不够的。要表示矩阵或复数等数学对象,必须在已有数据类型的基础上构造新的数据类型。这些新数据类型通常称为聚合(aggregate)或组合(composite)。\n\n数组是由相同类型的元素组成"
  },
  {
    "path": "content/part2/chapter5/3.tex",
    "chars": 1897,
    "preview": "通过向代码生成器添加数组和记录,有时生成的代码不会按预期执行,原因是我们忽略了平台的调用约定。对于同一个程序或库中的函数如何调用另一个函数,每个平台都定义了自己的规则。ABI文档中总结了这些规则,常见信息包括以下内容:\n\n\\begin{it"
  },
  {
    "path": "content/part2/chapter5/4.tex",
    "chars": 6826,
    "preview": "\n许多现代编程语言使用类支持面向对象,类是高级语言结构。本节中,我们将探讨如何将类结构映射到LLVM IR中。\n\n\\mySubsubsection{5.4.1.}{实现单继承}\n\n类是数据和方法的集合。一个类可以从另一个类继承,可能会添加更"
  },
  {
    "path": "content/part2/chapter5/5.tex",
    "chars": 230,
    "preview": "在本章中,了解了如何将聚合数据类型和指针转换为LLVM IR代码,应用程序二进制接口的复杂性,将类和虚函数转换为LLVM IR的不同方法。有了本章的知识,您将能够为编程语言创建一个LLVM IR代码生成器。\n\n下一章中,将了解一些有关IR生"
  },
  {
    "path": "content/part2/chapter6/0.tex",
    "chars": 399,
    "preview": "\n通过前面章节中介绍的IR生成,已经可以实现编译器中所需的大部分功能。本章中,将研究一些编译器中经常出现的高级主题。例如,许多现代语言都使用异常处理,将研究如何将其转换为LLVM IR。\n\n为了支持LLVM优化器,使其能够在某些情况下生成更"
  },
  {
    "path": "content/part2/chapter6/1.tex",
    "chars": 13737,
    "preview": "LLVM IR 中的异常处理与平台支持密切相关,将使用libunwind来了解最常见的异常处理类型。C++可以充分发挥libunwind的潜力,先来看一个C++示例,其中bar()函数可以抛出一个int或double值:\n\n\\begin{c"
  },
  {
    "path": "content/part2/chapter6/2.tex",
    "chars": 8467,
    "preview": "\n两个指针可能指向同一个存储单元,在这一点上它们彼此别名。内存在LLVM模型中没有类型,这使得优化器很难确定两个指针是否相互别名。若编译器可以证明两个指针不会相互别名,就可以进行更多的优化。在下一节中,我们将更深入地研究这个问题,并研究在实"
  },
  {
    "path": "content/part2/chapter6/3.tex",
    "chars": 17189,
    "preview": "\n为了允许源代码级调试,必须添加调试信息。LLVM中对调试信息的支持使用调试元数据来描述源语言的类型和其他静态信息,并使用intrinsic来跟踪变量值。LLVM核心库在Unix系统上以DWARF格式生成调试信息,在Windows系统上以P"
  },
  {
    "path": "content/part2/chapter6/4.tex",
    "chars": 223,
    "preview": "本章中,了解了如何在LLVM中抛出和捕获异常,以及如何生成IR来利用此功能。为了增强IR的范围,了解了如何将各种元数据添加到指令。用于基于类型的别名分析的元数据为LLVM优化器提供信息,并帮助进行某些优化以生成更好的机器代码。用户总是喜欢使"
  },
  {
    "path": "content/part2/chapter7/0.tex",
    "chars": 464,
    "preview": "\nLLVM使用一系列的通道来优化IR,一个通道在IR的一个单元上运行,例如一个函数或一个模块。操作可以是转换,以定义的方式更改IR,也可以是分析,其收集诸如依赖关系之类的信息。这一系列的通道称为通道流水线,通道管理器在编译器生成的IR上执行"
  },
  {
    "path": "content/part2/chapter7/1.tex",
    "chars": 86,
    "preview": "\n本章使用的代码在这里\\url{https://github.com/PacktPublishing/Learn-LLVM-17/tree/main/Chapter07}。"
  },
  {
    "path": "content/part2/chapter7/2.tex",
    "chars": 1093,
    "preview": "LLVM核心库优化编译器创建的IR并将其转换为目标代码。这项艰巨的任务需要分解成一个个独立的步骤,称为“通道”(passes)。这些通道需要以正确的顺序执行,这是通道管理器的目标。\n\n为什么不硬编码通道的顺序呢?编译器的用户通常希望编译器提"
  },
  {
    "path": "content/part2/chapter7/3.tex",
    "chars": 9419,
    "preview": "\n一个通道可以在LLVM IR上执行任意复杂度的转换。为了说明添加新通道的机制,我们添加一个执行简单检测的通道。\n\n为了研究程序的性能,了解函数被调用的频率和运行的时间是很有趣的。收集这些数据的一种方法是在每个函数中插入计数器,这个过程称为"
  },
  {
    "path": "content/part2/chapter7/4.tex",
    "chars": 7613,
    "preview": "回想一下我们在ppprofiler通道开发为插件一节中,从LLVM源码树中作为插件开发的ppprofiler通道。这里,因为LLVM工具可以加载插件,将学习如何将此通道与LLVM工具(如opt和clang)一起使用。\n\n我们先看一下opt。"
  },
  {
    "path": "content/part2/chapter7/5.tex",
    "chars": 9932,
    "preview": "\n我们在前面章节中开发的tinylang编译器,但没有对IR代码进行优化。接下来的小节中,我们将在编译器中添加一个优化流水线来实现这一目标。\n\n\\mySubsubsection{7.5.1.}{创建优化流水线}\n\nPassBuilder类是"
  },
  {
    "path": "content/part2/chapter7/6.tex",
    "chars": 198,
    "preview": "本章中,了解了如何为LLVM创建一个新的通道,使用通道流水线描述和扩展点运行通道。通过构造和执行类似于clang的通道流水线扩展了编译器,将tinylang变成了一个优化编译器。通道流水线允许在扩展点添加通道,并且了解了如何在这些点注册通道"
  },
  {
    "path": "content/part2/part2.tex",
    "chars": 262,
    "preview": "继续了解如何开发自己的编译器。将从构建前端开始,读取源文件并创建源文件的抽象语法树,如何从源文件生成LLVM IR。使用LLVM的优化功能,创建优化的机器码。此外,还会探索几个高级主题,包括为面向对象语言构造生成LLVM IR和添加调试元数"
  },
  {
    "path": "content/part3/chapter10/0.tex",
    "chars": 441,
    "preview": "LLVM有一组工具,可以发现应用程序中的某些错误,所有这些工具都使用了LLVM和clang库。\n\n本章中,料及如何使用消毒工具来检查应用程序,如何使用最常见的消毒工具来识别各种各样的bug,之后为应用程序实现模糊测试,这将检查通常在单元测试"
  },
  {
    "path": "content/part3/chapter10/1.tex",
    "chars": 280,
    "preview": "要在XRay性能分析部分创建火焰图,需要从\\url{https://github.com/brendangregg/FlameGraph}安装脚本。有些系统,比如Fedora和FreeBSD,为这些脚本提供了一个包,也可以使用。\n\n若要在同"
  },
  {
    "path": "content/part3/chapter10/2.tex",
    "chars": 6296,
    "preview": "\nLLVM具有几个消毒器,这些通道使用中间表示(IR)来检查应用程序的某些错误行为。通常,它们需要库支持,这是compiler-rt项目的一部分。消毒器可以在clang中启用,要构建编译器-rt项目,可以简单地在构建LLVM时将CMake变"
  },
  {
    "path": "content/part3/chapter10/3.tex",
    "chars": 3647,
    "preview": "\n要测试应用程序,需要编写单元测试,这是确保软件正常运行的好方法。然而,由于可能输入的指数数量,可能会错过某些奇怪的输入,以及一些错误。\n\n模糊测试在这方面可以提供帮助。其思想是为应用程序提供随机生成的数据,或者基于有效输入但随机更改的数据"
  },
  {
    "path": "content/part3/chapter10/4.tex",
    "chars": 4261,
    "preview": "若应用程序似乎运行缓慢,可能想知道代码中的时间花在了哪里,可以用XRay检测代码可以帮助完成这项任务。每个函数进入和退出时,都会向运行时库插入一个特殊的调用。允许计算函数调用的频率,以及在函数中花费的时间。可以在llvm/lib/XRay/"
  },
  {
    "path": "content/part3/chapter10/5.tex",
    "chars": 14571,
    "preview": "\nclang静态分析器是一个对C、C++和Objective C源代码执行额外检查的工具。静态分析器执行的检查比编译器执行的检查更彻底。在时间和所需资源方面,成本也更高。静态分析器有一组检查器,用于检查某些错误。\n\n该工具执行源代码的符号解"
  },
  {
    "path": "content/part3/chapter10/6.tex",
    "chars": 8128,
    "preview": "\n静态分析器是一个令人印象深刻的例子,说明了可以使用clang基础设施做些什么。也可以使用插件扩展clang,这样就可以将自己的功能添加到clang中。该技术非常类似于在LLVM中添加一个通道插件。\n\n我们用一个简单的插件来探索这个功能,L"
  },
  {
    "path": "content/part3/chapter10/7.tex",
    "chars": 340,
    "preview": "\n本章中,了解了如何使用各种消毒器。使用地址消毒器检测指针错误,使用内存消毒器检测未初始化的内存访问,并使用线程消毒器执行数据争用。应用程序错误通常是由格式不正确的输入触发的,所以实现了模糊测试,用随机数据测试应用程序。\n\n还使用XRay对"
  },
  {
    "path": "content/part3/chapter8/0.tex",
    "chars": 394,
    "preview": "LLVM中的大部分后端都用TableGen语言编写,TableGen是一种特殊的语言,用于生成C++源代码的片段,以避免每个后端都实现类似的代码,并减少源码量,所以了解TableGen很重要。\n\n本章中,将了解以下内容:\n\n\\begin{i"
  },
  {
    "path": "content/part3/chapter8/1.tex",
    "chars": 87,
    "preview": "本章使用的代码在这里, \\url{https://github.com/PacktPublishing/Learn-LLVM-17/tree/main/Chapter08}。"
  },
  {
    "path": "content/part3/chapter8/2.tex",
    "chars": 1023,
    "preview": "LLVM有自己的领域特定语言(DSL),称为TableGen,用于为很多用例生成C++代码,减少开发人员手写的代码量。TableGen语言不是一种编程语言,只用于定义记录。对于名称和值的集合来说,这是一个花哨的词。为了理解为什么使用这种语言"
  },
  {
    "path": "content/part3/chapter8/3.tex",
    "chars": 8078,
    "preview": "\n初学者对TableGen语言感到不知所措,开始尝试这门语言后,就会发现没那么难。\n\n\\mySubsubsection{8.3.1.}{定义记录和类}\n\n让我们为指令定义一条简单的记录:\n\n\\begin{shell}\ndef ADD {\n "
  },
  {
    "path": "content/part3/chapter8/4.tex",
    "chars": 16050,
    "preview": "\n上一节中,用TableGen语言定义了记录。使用这些记录,需要编写自己的TableGen后端,该后端可以生成C++源代码或使用记录作为输入执行其他操作。\n\n第3章中,Lexer类的实现使用数据库文件来定义标记和关键字,各种查询函数使用该数"
  },
  {
    "path": "content/part3/chapter8/5.tex",
    "chars": 371,
    "preview": "\n以下是TableGen的一些缺点:\n\n\\begin{itemize}\n\\item\nTableGen语言建立在一个简单的概念之上,所以不具有与其DSL相同的计算能力。显然,一些程序员希望用一种不同的、更强大的语言来取代TableGen,这个"
  },
  {
    "path": "content/part3/chapter8/6.tex",
    "chars": 159,
    "preview": "\n本章中,首先了解了TableGen背后的主要思想,并用TableGen语言定义了第一个类和记录,了解了TableGen语法的知识。最后,基于所定义的TableGen类,开发了用于生成C++源码的TableGen后端。\n\n下一章中,我们将研"
  },
  {
    "path": "content/part3/chapter9/0.tex",
    "chars": 430,
    "preview": "\nLLVM核心库拥有ExecutionEngine组件,该组件允许在内存中编译和执行中间表示(IR)代码。使用这个组件,可以构建即时(JIT)编译器,其允许直接执行IR代码。因为不需要将目标代码存储在辅助存储器上,所以JIT编译器的工作方式"
  },
  {
    "path": "content/part3/chapter9/1.tex",
    "chars": 87,
    "preview": "本章使用的代码在这里, \\url{https://github.com/PacktPublishing/Learn-LLVM-17/tree/main/Chapter09}。"
  },
  {
    "path": "content/part3/chapter9/2.tex",
    "chars": 1315,
    "preview": "\n目前,我们只讨论了提前(AOT)编译器,这些编译器编译整个应用程序。应用程序只能在编译完成后运行,若编译是在应用程序的运行时执行的,编译器就是JIT编译器。JIT编译器有一些有趣的用例:\n\n\\begin{itemize}\n\\item\n实现"
  },
  {
    "path": "content/part3/chapter9/3.tex",
    "chars": 2097,
    "preview": "\n\n\n考虑JIT编译器时,首先想到的是直接运行LLVM IR。这就是lli工具、LLVM解释器和动态编译器所做的工作。我们将在下一节中探讨lli工具。\n\n\n\\mySubsubsection{9.3.1.}{探索lli工具}\n\n让我们用一个非"
  },
  {
    "path": "content/part3/chapter9/4.tex",
    "chars": 13204,
    "preview": "\nlli工具只不过是LLVM API的一个包装。第一部分中,我们了解了ORC引擎使用分层方法。ExecutionSession类表示一个正在运行的JIT程序。除了其他项之外,这个类还保存诸如使用过的JITDylib实例之类的信息。JITDy"
  },
  {
    "path": "content/part3/chapter9/5.tex",
    "chars": 14207,
    "preview": "\n使用ORC的分层方法,很容易构建针对需求定制的JIT编译器。没有放之四海而皆准的JIT编译器,本章的第一节给出了一些示例。让我们看一下如何从头开始设置JIT编译器。\n\nORC API使用堆叠在一起的层。最低层是对象链接层,由llvm::o"
  },
  {
    "path": "content/part3/chapter9/6.tex",
    "chars": 170,
    "preview": "本章中,了解了如何开发JIT编译器。首先了解了JIT编译器的可能应用程序,探索了LLVM动态编译器和解释器。使用预定义的LLJIT类,构建了一个交互式的基于jit的计算器工具,并了解了如何查找符号和向LLJIT添加IR模块。为了能够利用OR"
  },
  {
    "path": "content/part3/part3.tex",
    "chars": 242,
    "preview": "深入研究LLVM的各种底层细节,探索TableGen语言,这是LLVM的特定于领域的语言,并了解如何在后端使用它。LLVM还有一个即时(JIT)编译器,将探索如何使用,并根据需求进行定制。此外,还将尝试用于识别应用程序中的错误的各种工具和库"
  },
  {
    "path": "content/part4/chapter11/0.tex",
    "chars": 507,
    "preview": "LLVM具有非常灵活的架构,可以添加一个新的目标后端。后端的核心是目标描述,大部分代码都是从目标描述中生成的。本章中,将学习如何添加对历史CPU的支持。\n\n本章中,将学习以下主题:\n\n\\begin{itemize}\n\\item\n为创建一个新"
  },
  {
    "path": "content/part4/chapter11/1.tex",
    "chars": 712,
    "preview": "无论是商业上需要支持一个新的CPU,还是一个爱好项目来增加对一些旧架构的支持,向LLVM添加新的后端都是一项主要任务。本章和接下来的两章概述了为新后端开发所需的内容,我们将为摩托罗拉M88k架构添加一个后端,其为20世纪80年代的RISC架"
  },
  {
    "path": "content/part4/chapter11/2.tex",
    "chars": 697,
    "preview": "Triple类的一个实例表示LLVM为之生成代码的目标平台。为了支持新架构,第一个任务是扩展Triple类。llvm/include/llvm/TargetParser/Triple.h文件中,添加一个成员到ArchType枚举和一个新谓词"
  },
  {
    "path": "content/part4/chapter11/3.tex",
    "chars": 2268,
    "preview": "ELF文件格式是LLVM支持的二进制对象文件格式之一。ELF本身为许多CPU架构定义,M88k架构也有定义。我们所需要做的就是,添加重定位的定义和一些标志。重新定位在《System V ABI M88k处理器补充》的第4章,IR代码生成基础"
  },
  {
    "path": "content/part4/chapter11/4.tex",
    "chars": 8070,
    "preview": "\n目标描述是后端实现的核心,使用TableGen语言编写,并定义了架构的基本属性,例如:寄存器和指令格式以及用于指令选择的模式。若不熟悉TableGen语言,建议先阅读本书第8章。基本定义在llvm/include/llvm/Target/"
  },
  {
    "path": "content/part4/chapter11/5.tex",
    "chars": 4338,
    "preview": "我们还没有讨论在哪里放置目标描述文件,LLVM中的每个后端在llvm/lib/Target中都有一个子目录。这里创建M88k目录,并将目标描述文件复制到其中。\n\n当然,仅添加TableGen文件是不够的。LLVM使用注册表查找目标实现的实例"
  },
  {
    "path": "content/part4/chapter11/6.tex",
    "chars": 17739,
    "preview": "\n汇编器解析器很容易实现,LLVM为它提供了一个框架,而且大部分都是从目标描述生成的。\n\n当框架检测到需要解析指令时,就会调用类中的ParseInstruction()方法,通过提供的词法分析器解析输入,并构造一个所谓的操作数向量。操作数可"
  },
  {
    "path": "content/part4/chapter11/7.tex",
    "chars": 3043,
    "preview": "实现反汇编器是可选的,但实现并不需要太多的努力,并且生成反汇编表可能会捕获其他生成器未检查的编码错误。反汇编程序位于m88kdisassemer.cpp文件中,位于Disassembler子目录中:\n\n\\begin{enumerate}\n\\"
  },
  {
    "path": "content/part4/chapter11/8.tex",
    "chars": 197,
    "preview": "本章中,了解了如何创建LLVM目标描述,并开发了一个简单的后端目标,该目标支持LLVM指令的汇编和反汇编。首先收集了所需的文档,并通过增强Triple类使LLVM了解新的架构。该文档还包括ELF文件格式的重定位定义,并将对它们的支持添加到L"
  },
  {
    "path": "content/part4/chapter12/0.tex",
    "chars": 488,
    "preview": "任何后端的核心都是指令选择,LLVM实现了几种方法。本章中,我们将通过选择有向无环图(DAG)和全局指令选择来实现指令选择。\n\n本章中,将学习以下主题:\n\n\\begin{itemize}\n\\item\n定义调用约定的规则:如何在目标描述中,描"
  },
  {
    "path": "content/part4/chapter12/1.tex",
    "chars": 1544,
    "preview": "\n实现调用约定的规则是,将LLVM中间表示(IR)降为机器码的重要组成部分,基本规则可以在目标描述中定义。\n\n大多数调用约定遵循一个基本模式:定义一个寄存器子集用于参数传递。若这个子集没有耗尽,则在下一个空闲寄存器中传递下一个参数。若没有空"
  },
  {
    "path": "content/part4/chapter12/2.tex",
    "chars": 10111,
    "preview": "\n\n后端从IR创建机器指令是一项非常重要的任务。实现它的一种常见方法是利用DAG:\n\n\\begin{enumerate}\n\\item\n首先,必须从IR创建DAG。DAG的一个节点表示一个操作,边缘表示控制和数据流依赖关系。\n\n\\item\n接"
  },
  {
    "path": "content/part4/chapter12/3.tex",
    "chars": 4456,
    "preview": "\n目标描述捕获有关寄存器和指令的大部分信息。要访问该信息,必须实现M88kRegisterInfo和M88kInstrInfo类。这些类还包含钩子,可以覆盖这些钩子来完成那些太复杂,而无法在目标描述中表达的任务。从M88kRegisterI"
  },
  {
    "path": "content/part4/chapter12/4.tex",
    "chars": 1389,
    "preview": "平台的二进制接口不仅定义了参数的传递方式,还包括如何布局堆栈帧:哪里存储局部变量,将寄存器溢出到哪里等。函数的开始和结束处需要一个特殊的指令序列,称为序言和尾声。当前的开发状态下,目标不支持创建序言和尾部声明的机器指令,但用于指令选择的帧代"
  },
  {
    "path": "content/part4/chapter12/5.tex",
    "chars": 2908,
    "preview": "指令选择从LLVM IR中创建机器指令,由MachineInstr类表示,但这并不是结束。MachineInstr类的实例仍然携带额外的信息,比如标签或标志。为了通过机器码组件发出一条指令,需要将MachineInstr的实例降低为MCIn"
  },
  {
    "path": "content/part4/chapter12/6.tex",
    "chars": 9056,
    "preview": "\n我们已经实现了指令选择类和一些其他类。现在,需要设置后端如何工作。与优化流水线一样,后端也分为几个通道。配置这些通道是M88kTargetMachine类的主要任务,所以需要指定哪些特性可用于指令选择。通常,平台是一系列CPU,它们都有一"
  },
  {
    "path": "content/part4/chapter12/7.tex",
    "chars": 8840,
    "preview": "\n通过选择DAG进行指令选择可以生成快速的代码,但是这样做需要时间。对于开发人员来说,编译器的速度至关重要,他们想要快速地尝试他们所做的更改。通常,编译器在优化级别0时应该非常快,但是随着优化级别的增加,可能会花费更多的时间。但构造选择DA"
  },
  {
    "path": "content/part4/chapter12/8.tex",
    "chars": 545,
    "preview": "使用本章和前一章的代码,创建了一个后端,可以将一些LLVM IR转换为机器码。看到后端能够正常工作是非常令人满意的,但不能用于实际任务。需要编写更多的代码。以下是如何进一步发展后端的秘诀:\n\n\\begin{itemize}\n\\item\n第一"
  },
  {
    "path": "content/part4/chapter12/9.tex",
    "chars": 275,
    "preview": "本章中,后端添加了两种不同的指令选择:通过选择DAG进行指令选择和全局指令选择,所以必须在目标描述中定义调用约定。此外,需要实现寄存器和指令信息类,能够访问从目标描述生成的信息,但还需要使用其他信息对其进行增强,了解到堆栈帧布局和脚本生成稍"
  },
  {
    "path": "content/part4/chapter13/0.tex",
    "chars": 315,
    "preview": "现在已经在前面的章节中,了解了使用SelectionDAG和GlobalISel基于LLVM的框架进行指令选择,可以探索指令选择之外的其他有趣概念。本章封装了后端之外的一些更高级的主题,这些主题对于高度优化的编译器来说可能会很有趣。例如,一"
  },
  {
    "path": "content/part4/chapter13/1.tex",
    "chars": 12674,
    "preview": "\n本节中,我探讨如何在LLVM中实现一个在指令选择后运行的新机器函数通道。将创建一个MachineFunctionPass类,它是LLVM中原始FunctionPass类的一个子集,可以与opt一起运行。该类调整了原始基础设施,以通过llc"
  },
  {
    "path": "content/part4/chapter13/2.tex",
    "chars": 17197,
    "preview": "\n前面的章节中,我们在LLVM中开发了M88k目标的后端实现。为了完成M88k目标的编译器实现,通过为M88k目标添加clang实现来研究将新目标连接到前端。\n\n\\mySubsubsection{13.2.1.}{clang中实现驱动程序的"
  },
  {
    "path": "content/part4/chapter13/3.tex",
    "chars": 4135,
    "preview": "现如今,许多小型计算机(如树莓派),只有有限的资源。在这样的计算机上运行编译器通常是不可能的,或者需要花费太多时间。因此,编译器的一个常见需求是为不同的CPU架构生成代码。让主机为不同的目标编译可执行文件的整个过程,称为交叉编译。\n\n交叉编"
  },
  {
    "path": "content/part4/chapter13/4.tex",
    "chars": 269,
    "preview": "本章中,了解了如何创建超越指令选择的通道,如何在后端创建机器函数通道!还了解了如何向clang中添加新的实验目标,以及需要对驱动程序、ABI和工具链进行的一些更改。最后,在考虑编译器构造的最高原则时,了解了如何为另一个目标架构交叉编译应用程"
  },
  {
    "path": "content/part4/part4.tex",
    "chars": 189,
    "preview": "了解如何使用TableGen语言为LLVM不支持的CPU架构添加新的后端目标,还将探索LLVM中的各种指令选择框架,并了解如何实现。最后,将深入研究LLVM中指令选择框架之外的概念,这些概念对于优化后端很有价值。\n\n\\begin{itemi"
  }
]

About this extraction

This page contains the full source code of the xiaoweiChen/Learn-LLVM-17 GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 104 files (416.1 KB), approximately 179.5k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.

Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.

Copied to clipboard!