LET'S BUILD A COMPILER!(2)

本文围绕构建编译器展开,聚焦表达式分析与翻译。从单个数字表达式入手,逐步扩展到二分表达式、一般表达式,还引入栈处理复杂情况,最后处理乘法和除法运算符。虽生成代码效率低,但能正确识别合法语句、给出错误信息,且可通过少量代码扩展功能。
部署运行你感兴趣的模型镜像

                                LET'S BUILD A COMPILER

                                                  By

                                    Jack W. Crenshaw, Ph.D.

                                    第二部分:表达式分析

开始

    假如你已经读过了本系列教程地入门篇,你就应该知道我们下一步将要做什么。你应该已经将cradle程序拷贝到你的Turbo Pascal中,并成功地编译了它。所以你应该已准备好开始新的学习了。
    这篇文章的目的是学习如何分析和翻译数学表达式。我们定义的输出是一系列能够完成指定操作的汇编语句。在这里,我们定义表达式是写在等式的右边的部分,比如
 
             
x = 2*y + 3/(4*z)

    开始时我们的进展的速度会很慢。那是为了使初学者不至于迷失方向。如果有些课程你以前学过,那么对后面的学习将会有很大好处。对那些稍有经验的读者,我要说:请暂且忍耐。我们稍后将会加快进度。

单个数字

    为了保持本教程的一贯风格(KISS准则,还记得吗?)我们从最简单的例子入手。那是一个仅由单个数字组成的表达式。
    开始编码之前,确保你已经有了我上次所给的那个例程。在其他试验里我们还会用到它。在例程中加入以下代码:

{---------------------------------------------------------------}

{ Parse and Translate a Math Expression }

procedure Expression;

begin

  
EmitLn('MOVE #' + GetNum + ',D0')

end;

{---------------------------------------------------------------}

 
在主程序中加入一行“Expression;”。

{---------------------------------------------------------------}

begin

   Init;

   Expression;

end.

{---------------------------------------------------------------}

    现在运行这个程序。试着输入所有的单个数字做试验。得到的输出应该是一行汇编代码。现在试着输入任何其他的字符,你会看到分析器以恰当的方式报告了一个错误。

    祝贺你!你已经写了一个可以运行的翻译器了!OK,我承认它的功能是很有限的。但不要轻易地就把它扔掉。这个小小的编译器在一个非常有限的范围内精确地完成了大型编译器所要做的所有工作:它正确地识别了我们为之定义的输入语言中的合法语句,并且生成了正确的、可执行的汇编代码,这些汇编代码也适合翻译成目标语言格式。很重要的是,它也能正确地识别出非法语句,并给出有意义的错误信息。谁还能要求得更多呢?当我们扩展这个分析器时,我们最好使它一直保持这两个特性。

    这个小程序中还有其他的特点值得一提。首先,我们没有将代码生成和语法分析分离开……分析器一旦知道我们想要做什么,就直接生成目标代码。当然,在一个实际的编译器中,输入是用GetChar从磁盘文件读入,然后输出写到另一个磁盘文件中,但做试验时,我们使用的这种方法无疑容易得多。

    同时注意一个表达式必须产生一个结果放到某个地方。我选用68000D0寄存器来保存他们。我也可以选择别的寄存器,但用D0更合适。

 

二分表达式

    不可否认,只含一个字符的表达式离我们的要求太远了,所以现在来看看如何扩展它。假定我们要处理的表达式形式如下:      
                              
1+2

      
或者                  4-3   
      
 
或者,用通用的形式就是,<term> +/- <term>
   (那只是
Backus-Naur 范式, 或称BNF范式的一小部分。)

    为了做到这一步,我们需要一个过程来识别term和将结果保存在某处,还需要另一个过程来识别、分辨’+’’-‘,并生成适当的代码。但有个问题,如果Expression过程准备把它的结果放在DO寄存器中,Term过程的结果应该放在哪里呢?答案是:同样的地方。在得到第二个Term的结果之前,我们不得不把第一个结果在某处保存起来。

       OK,我们想要做的基本上就是让Term过程完成刚才Expression做的工作。所以只需要将Expression过程重命名为Term就可以了,并输入Expression过程的新版本。

{---------------------------------------------------------------}

{ Parse and Translate an Expression }

procedure Expression;

begin

   Term;

   EmitLn('MOVE D0,D1');

   case Look of

    '+': Add;

    '-': Subtract;

   else Expected('Addop');

   end;

end;

{--------------------------------------------------------------}

下面,在Expression过程之前输入下面两个过程。
{--------------------------------------------------------------}

{
识别并翻译一个加法运算}

procedure Add;

begin

   Match('+');

   Term;

   EmitLn('ADD D1,D0');

end;

{-------------------------------------------------------------}

{识别并翻译一个减法运算}

procedure Subtract;

begin

   Match('-');

   Term;

   EmitLn('SUB D1,D0');

end;

{-------------------------------------------------------------}

输入完上述各个过程后,它们之间的顺序应该是:

 o Term(实际上是原来的Expression版本)

 o Add

 o Subtract

 o Expression

    现在运行这个程序。试着输入你能想到的任何两个单数字,并把它们用’+’’-‘连接起来。每次运行都会生成四条汇编指令。现在试着输入一些精心设计的包含错误的表达式。分析器捕捉到这些错误了吗?

    看一下运行后生成的代码。有两处值得我们注意。首先,这些代码不像我们手写的代码,有点别扭。比如代码序列:
       
MOVE #n,D0

       
MOVE D0,D1

    它是低效的。假如我们手工写这些代码的话,我们一般会将数据直接放到D1中。

    关于这点有以下说明:编译器生成的代码一般比我们手写的代码效率要低。记住这点。它将会贯穿在我们的整个教程中。所有的编译器都不同程度地存在这个问题。许多计算机科学家将花了毕生心血来研究代码优化问题,也确实有一些成果可以用来提高目标代码质量。有些编译器在代码优化方面就做得很好,但是也由于其复杂性而卖得很贵,总之这是一个被遗忘的战场……在这部分结束之前,我会简要地介绍一些优化的方法,它们能完成一些轻度的优化,但这只是为了告诉你我们的确能够对代码做某些优化而不用费太多麻烦。请记住,这里我们只是为了学习,而不是为了检验我们到底能生成多么紧凑的目标代码。后面我们将故意忽略代码优化问题,将注意力集中在如何使生成的代码能正常的运行起来。

    但是现在我们的程序还不能做到这点!生成的代码还有错误!出错的地方是做减法时,从D0(保存第一个参数)中减去D1(保存第二个参数)。这种方法是错误的,所以最后结果的符号错误。下面完善Subtract过程,补充进对符号的修改,

{-------------------------------------------------------------}

{识别并翻译一个减法运算}
procedure Subtract;

begin

   Match('-');

   Term;

   EmitLn('SUB D1,D0');

   EmitLn('NEG D0');

end;

{-------------------------------------------------------------}

    现在我们的代码虽然效率低一点,但至少能给出正确的结果了!然而不幸的是,这种规则能表明数学表达式的意义,但表达式中term出现的次序我们却不太习惯。没办法,这只是你必须学会适应的的诸多方面之一。这一点我们在讲到除法时还会遇到。

       OK,到这一步我们已经能让分析器识别两数的和与差了。早先我们还只能让它识别单个数字。但是实际问题中碰到的表达式,其形式都是任意的。输入单数字“1”运行程序,看看有没有问题。

    它没有正常工作,是吗?为什么呢?因为前面我们告诉过分析器,表达式的唯一合法形式是有两个term的情况。我们必须重写Expression过程使其能处理更多的情况。一个真正的分析器快要成型了。

 

一般表达式

    在实际问题中,一个表达式可以由一个或多个term组成,其间用“加法运算符” ’+’‘-’)分割。用BNF的形式可以写作:

          <expression> ::= <term> [<addop> <term>]*

我们可以向Expression过程加入一个简单的循环来描述这个新定义。

{---------------------------------------------------------------}

{ 分析并翻译表达式}

procedure Expression;

begin

   Term;

   while Look in ['+', '-'] do begin

      EmitLn('MOVE D0,D1');

      case Look of

       '+': Add;

       '-': Subtract;

      else Expected('Addop');

      end;

   end;

end;

{--------------------------------------------------------------}

    现在我们已经前进了一大步!这个版本可以处理任意数目的term,并且我们只是增加了两条代码而已。进一步深入的话,你会发现这是自顶向下分析器的特性……它只需要增加少量的代码就可以处理对这种语言的扩展。这就使得我们对分析器逐步改进的方案有可能实现。也要注意到,Expression过程的代码与表达式的BNF定义是十分匹配的,这也是此种方法的一个特性。当你精通这种方法后,会发现你很快就能将一个BNF表达式转换成对应的分析器代码。

       OK,编译这个分析器的新版本,试运行它。像往常一样,验证这个“编译器“是否能处理任何合法的表达式,是否会对非法形式给出有意义的出错信息。它运行得还不错,是吗?你也许会发现在我们的试验版本中,出错信息是和生成的代码混杂在一起输出的那只是因为我们在试验中是使用的显示器作为输出。在实际的产品中,会有两个分割开的输出,一个是输出文件,一个是输出到屏幕。

 

使用栈

    以前我一直坚持在解释代码中的问题时,除非绝对有必要,否则不会引入任何复杂的理论,但是这里我准备打破这一点。目前的情况是,分析器使用D0作为主寄存器,用D1存放中间结果。迄今为止,它还工作良好,因为我们处理的只是“加法运算符”’+’’-‘,只要一发现有新的term出现,它就被加到结果当中。但一般来说这是不完善的。考虑一下这个表达式
              
1+(2-(3+(4-5)))

    假如我们把1放到D1中,那把2放到哪里?

    幸运的是,有个简单的解决办法。像任何现代的微处理器一样,68000也有实现栈的结构,而栈是保存一系列数据的绝佳场所。所以不需要把termD0转移到D1中,我们只要将它压入栈里就可以了。68000汇编中,压栈操作的语句是
                       
-(SP)

而出栈则写作,
(SP)+ .

    好,我们把Expression过程中的EmitLn语句改为:
              
EmitLn('MOVE D0,-(SP)');

    两条包含AddSubtract的语句分别改为
              
EmitLn('ADD (SP)+,D0')

           EmitLn('SUB (SP)+,D0'),

    现在试运行分析器,以保证我们的更改没有破坏它。现在生成的代码比以前效率更低了,但是这是不可避免的。

 

乘法和除法

    现在开始处理一些真正麻烦的事。你知道,除了“加法运算符”还有其他一些数学运算符……表达式还可以有乘法和除法运算符。你也知道,在表达式各运算符之间存在优先级的关系,所以如下表达式
                   
2 + 3 * 4,

我们会认为是先做乘法,后做加法。(回头看看为什么我们需要用栈?)

    在编译器技术发展的早期,人们用一些相当复杂的技术来实现各运算符之间的优先级关系。但那已经过时了,用自顶向下分析技术我们能漂亮地实现这些规则。现在为止,我们所考虑的term的形式一直都只是单个的十进制数字。

    更一般地,我们将term定义成由因子(factor)的组合所生成的东西,也就是说,

          <term> ::= <factor>  [ <mulop> <factor ]*

    什么是因子?现在我们对它的理解,因子就是过去讲的term……单个数字。

    注意如下的对称性:term与表达式有着同样的形式。实际上,我们可以把处理表达式的过程稍做修改并重新命名为处理因子的过程,然后添加到分析器中。但为了避免混淆,下面列出的是分析器的最新完整代码。(注意我们在Divide过程中处理顺序颠倒的操作数所用的方法。)

{---------------------------------------------------------------}

{ 分析并翻译一个因子 }

procedure Factor;

begin

   EmitLn('MOVE #' + GetNum + ',D0')

end;

{--------------------------------------------------------------}

{ 识别并翻译乘法运算}

procedure Multiply;

begin

   Match('*');

   Factor;

   EmitLn('MULS (SP)+,D0');

end;

{-------------------------------------------------------------}

{识别并翻译除法运算}

procedure Divide;

begin

   Match('/');

   Factor;

   EmitLn('MOVE (SP)+,D1');

   EmitLn('DIVS D1,D0');

end;

{---------------------------------------------------------------}

{分析并翻译Term}

procedure Term;

begin

   Factor;

   while Look in ['*', '/'] do begin

      EmitLn('MOVE D0,-(SP)');

      case Look of

       '*': Multiply;

       '/': Divide;

      else Expected('Mulop');

      end;

   end;

end;

{--------------------------------------------------------------}

{识别并翻译加法运算}

procedure Add;

begin

   Match('+');

   Term;

   EmitLn('ADD (SP)+,D0');

end;

{-------------------------------------------------------------}

{识别并翻译减法运算}

procedure Subtract;

begin

   Match('-');

   Term;

   EmitLn('SUB (SP)+,D0');

   EmitLn('NEG D0');

end;

{---------------------------------------------------------------}

{ 分析并翻译表达式}

procedure Expression;

begin

   Term;

   while Look in ['+', '-'] do begin

      EmitLn('MOVE D0,-(SP)');

      case Look of

       '+': Add;

       '-': Subtract;

      else Expected('Addop');

      end;

   end;

end;

{--------------------------------------------------------------}

    好家伙!一个接近于实用的分析器/翻译器,只有55行的pascal语言源程序!假如你继续忽略它的效率问题,那这个输出看起来就很有用,我也希望你忽略效率问题。记住,我们的目的并不是要生成很紧凑的代码。

您可能感兴趣的与本文相关的镜像

Dify

Dify

AI应用
Agent编排

Dify 是一款开源的大语言模型(LLM)应用开发平台,它结合了 后端即服务(Backend as a Service) 和LLMOps 的理念,让开发者能快速、高效地构建和部署生产级的生成式AI应用。 它提供了包含模型兼容支持、Prompt 编排界面、RAG 引擎、Agent 框架、工作流编排等核心技术栈,并且提供了易用的界面和API,让技术和非技术人员都能参与到AI应用的开发过程中

<think>嗯,用户现在问的是“modern-compiler”,也就是现代编译器。我需要先理解用户的需求是什么。可能他们想了解现代编译器的基本概念、结构或者最新的技术趋势。根据之前的对话历史,用户之前遇到了一个关于Vite和sass-embedded的动态加载问题,可能是在前端项目中的构建工具问题。因此,他们可能对编译器在构建工具中的应用感兴趣,或者想更深入地了解编译器技术以解决类似的问题。 首先,我应该定义什么是现代编译器。现代编译器与传统编译器相比,有哪些改进和新的特性?可能需要提到编译速度、模块化设计、支持的语言特性、优化技术以及集成开发环境(IDE)的支持等。另外,现代编译器可能更注重与开发工具的集成,比如静态分析、代码提示、实时错误检测等。 然后,结构方面,现代编译器通常分为哪些阶段?前端、中端、后端,每个阶段的具体任务是什么?比如词法分析、语法分析、语义分析、中间代码生成、优化和目标代码生成。用户可能需要了解这些阶段是如何协同工作的,以及每个阶段的技术细节。 接下来,现代编译器的关键技术可能包括JIT(即时编译)、AOT(预先编译)、多阶段优化、跨平台支持、插件化架构等。这些技术如何提升编译器的性能和灵活性?例如,JIT在运行时优化代码执行,而AOT可以提高启动速度。跨平台支持让编译器能够生成不同环境的代码,适应多样化的部署需求。 然后,可能需要介绍一些现代编译器的实例,比如LLVM、Roslyn(C#)、Rust编译器、TypeScript编译器、Babel等。这些编译器各自的特点和应用场景是什么?比如LLVM的模块化设计,Rust的所有权系统如何影响其编译过程,TypeScript如何将TS代码转换为JS等。 用户可能的深层需求是希望了解如何选择或优化编译器,或者在自己的项目中应用现代编译器的技术。例如,他们可能在开发前端项目,使用Vite等工具时遇到性能问题,想了解编译器优化如何帮助提升构建速度。或者他们可能对学习编译器开发感兴趣,想了解现代编译器的设计理念。 此外,用户可能对现代编译器在云计算、微服务、WebAssembly等新兴技术中的应用感兴趣。比如,WebAssembly如何利用现代编译器技术实现跨浏览器的高性能执行,或者编译器在Serverless架构中的作用。 需要注意的是,用户可能不是编译器专家,所以解释需要通俗易懂,避免过于专业的术语,或者至少在使用时给出简单解释。同时,结合之前的错误处理经验,用户可能更关注实际应用和解决方案,所以可以强调现代编译器如何解决实际开发中的问题,比如模块化依赖、构建速度优化等。 最后,总结现代编译器的发展趋势,比如AI在编译优化中的应用,自动化代码生成,以及更加智能的错误提示和调试支持。这些内容可以帮助用户了解未来的发展方向,以及如何利用这些趋势提升自己的开发效率。</think>以下是关于现代编译器的详细介绍,分为技术原理、核心架构、典型应用三个层次展开: ### 一、现代编译器核心架构 $$ \begin{array}{|c|c|c|} \hline \text{前端} & \text{中端} & \text{后端} \\ \hline \text{词法分析} & \text{中间表示(IR)} & \text{目标代码生成} \\ \text{语法分析} & \text{控制流分析} & \text{指令选择} \\ \text{语义分析} & \text{数据流分析} & \text{寄存器分配} \\ \text{AST生成} & \text{优化器} & \text{代码调度} \\ \hline \end{array} $$ #### 关键演进特征: 1. **模块化设计**:LLVM 的三大阶段分离架构 2. **多阶段优化**:从传统 peephole 优化发展到全局优化 3. **即时编译(JIT)**:V8 引擎的隐藏类优化技术 4. **跨平台支持**:WebAssembly 编译器的通用二进制格式 ### 二、典型现代编译器实现 ```rust // Rust 编译器 (rustc) 工作流程示例 fn compile(source: &str) -> Executable { let ast = parser::parse(source); // 语法分析 let hir = lower_to_hir(ast); // 高级中间表示 let mir = build_mir(hir); // 中级中间表示 let llvm_ir = codegen(mir); // LLVM IR生成 llvm::compile_to_machine_code(llvm_ir) // 机器码生成 } ``` #### 现代编译器关键技术矩阵: | 技术领域 | 典型实现 | 性能提升指标 | |----------------|---------------------------|---------------| | 增量编译 | Roslyn(C#) | 构建时间↓70% | | 并行编译 | Go 1.21+ | 核心利用率↑90%| | 分层编译 | Java JVM TieredCompilation| 启动速度↑50% | | 缓存系统 | Bazel Remote Cache | 重复编译↓99% | ### 三、前沿编译技术应用 $$ \text{AI辅助编译优化流程} \\ \begin{cases} \text{代码特征提取} \xrightarrow{ML模型} \text{优化策略预测} \\ \text{运行时Profile数据} \rightarrow \text{动态重优化} \\ \text{自动向量化} \xrightarrow{SIMD指令} 4\times \text{加速比} \end{cases} $$ #### 典型应用场景: 1. **WebAssembly**:Emscripten 将 C/C++ 编译为 WASM ```cpp // 示例:C代码编译为WASM #include <emscripten.h> EMSCRIPTEN_KEEPALIVE int add(int a, int b) { return a + b; } ``` ```bash emcc -O3 -s WASM=1 -o math.wasm math.c ``` 2. **智能合约编译**:Solidity → EVM字节码 ```solidity // Solidity智能合约示例 contract Voting { mapping(bytes32 => uint256) public votes; function vote(bytes32 candidate) public { votes[candidate] += 1; } } ``` ### 四、现代编译器技术挑战 1. **异构计算支持**:NVIDIA CUDA 编译器对 GPU 的 PTX 代码生成 2. **安全编译**:Rust 的所有权系统编译时检查 ```rust fn main() { let s = String::from("hello"); let s1 = s; // 所有权转移 println!("{}", s); // 编译错误:value borrowed after move } ``` 3. **量子编译**:Q# 编译器到量子电路转换 ### 五、开发工具集成 $$ \text{现代编译器生态} = \begin{cases} \text{LSP协议} & \text{实时代码分析} \\ \text{DAP调试器} & \text{编译时元数据集成} \\ \text{IDE插件} & \text{即时错误反馈} \end{cases} $$ #### 性能对比(GCC 13 vs LLVM 17): | 测试套件 | 编译时间(s) | 代码大小(KB) | 执行时间(s) | |----------------|-------------|--------------|-------------| | SPECint 2017 | 382 vs 401 | 1542 vs 1486 | 89 vs 85 | | 内核编译 | 218 vs 245 | 112MB vs 109MB | - | 现代编译器正朝着智能化(AI驱动)、专业化(领域特定语言)、安全化(形式化验证)方向发展。最新趋势包括: 1. 基于 MLIR 的多层级中间表示 2. 自动差异化编译(PyTorch 2.0 JIT) 3. 完全形式化验证的编译器(CompCert C)
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值