手写一个PrattParser基本运算解析器1: 编译原理概述

news2025/1/18 9:49:44

点击查看 基于Swift的PrattParser项目


编译原理概述

编译原理是我们每一个程序猿必须要了解的技能, 编译原理实际上并没有啥高深的技术, 我们如果在做业务开发, 也很少会用到编译开发的知识, 但是编译原理又是我们必备的基础知识之一. 所以我们需要对编译原理的内容有一个大概的了解.

其实我自己写这一个系列的起因, 一个是我恶补编译原理的相关内容, 另外一个就是看到了B站熊爷的技术去魅篇- 手写一个普拉特解析器, 感觉这个对我辅助理解遍历原理的一个很好的方案. 所以才会有这一个系列的博客.

首先就是编译过程中的基本组成结构单元. 如下图所示.

基本组成结构主要有 词法分析, 语法分析, 语义分析, 中间代码生成, 代码优化, 目标代码生成, 符号表管理, 错误管理.

下面, 我们就以下运算代码(以 Clang 作为编译器为例)为示例, 写一些伪过程来了解整个编译过程.

1 + 3 * 2
  • 词法分析(Lexical Analysis)

    词法分析主要是将源代码分解为词法单元(tokens),如关键字标识符运算符等。词法分析器负责读取字符流,根据语法规则识别和分类词法单元。

    使用下面的命令(需要安装Mac xcode, 以提供 Clang 编译前端环境)

    clang -Xclang -dump-tokens helloworld.c
    

    上面的运算代码会被划分成如下的词法单元. (会报错, 但不影响)

    numeric_constant '1'	 [StartOfLine]	Loc=<helloworld.c:1:1>
    
    plus '+'	 [LeadingSpace]	Loc=<helloworld.c:1:3>
    
    numeric_constant '3'	 [LeadingSpace]	Loc=<helloworld.c:1:5>
    
    star '*'	 [LeadingSpace]	Loc=<helloworld.c:1:7>
    
    numeric_constant '2'	 [LeadingSpace]	Loc=<helloworld.c:1:9>
    
    eof ''		Loc=<helloworld.c:1:11>
    

    Loc 代表着词法单元在源文件中的位置, 所以实际上 1 + 3 * 2 被划分成以下的词法单元.


  • 语法分析(Syntax Analysis)

    将词法单元流转换为抽象语法树(Abstract Syntax Tree,AST)。语法分析器负责检查和验证语法结构,根据语法规则构建语法树,以便后续的语义分析和代码生成。

    上一个步骤我们已经把源文件划分成了一个个的词法单元, 但是执行顺序呢? 因为在数学运算过程中 乘法 要比 加法 优先级高, 所以我们在 1 + 3 * 2的运算过程中. 需要先计算 3 * 2 而不是 1 + 3, 我们由于有数学基础, 一眼就知道应该先计算谁, 但程序是如何知道应该先计算谁呢? 所以我们需要构建AST语法树, 为什么要构建AST语法树, 我们实际操作一下你就明白了.

    注: 由于 1 + 3 * 2 构建语法树不是很直观, 我们把 helloworld.c 中的内容改为 int a = 1 + 3 * 2;

    使用下面的命令来构建AST语法树.

    clang -Xclang -ast-dump -fsyntax-only helloworld.c
    

    抛去一些别的构建树节点, 我们会发现如下的结构.

    `-VarDecl 0x7fb69188e200 <helloworld.c:1:1, col:17> col:5 a 'int' cinit
        `-BinaryOperator 0x7fb69188e330 <col:9, col:17> 'int' '+'
            |-IntegerLiteral 0x7fb69188e2b0 <col:9> 'int' 1
            `-BinaryOperator 0x7fb69188e310 <col:13, col:17> 'int' '*'
            |-IntegerLiteral 0x7fb69188e2d0 <col:13> 'int' 3
            `-IntegerLiteral 0x7fb69188e2f0 <col:17> 'int' 2
    

    上面看着很杂乱, 我们稍微改造一下, 再看一下, 整体如下图所示.

    通过上图, 我们就知道AST语法树实际上是类似于二叉树的结构. 我们可以通过生成的AST语法树知道, 如果想要执行 图中的加法运算, 就必须先执行 图中的乘法运算, 这也就是为什么计算机能通过AST语法树知道操作的优先级.


  • 语义分析(Semantic Analysis)

    对语法树进行语义检查,包括类型检查、作用域检查等。语义分析器负责分析语句和表达式的含义和关联,确保程序在运行时不会出现语义错误。

    在iOS开发过程中, 语义分析也叫做静态分析. 计算机做的操作也是包括类型检查实现检查(某个类是否存在某个方法)变量使用,还会有一些 复杂的检查.

    例如在 Objective-C 中,给某一个对象发送消息(调用某个方法),检查这个对象的类是否声明这个方法(但并不会去检查这个方法是否实现,这个错误是在运行时进行检查的),如果有什么错误就会进行提示。


  • 中间代码生成(Intermediate Code Generation)

    将语法树转换为中间表示形式,如三地址码、虚拟指令等。中间代码生成器负责将高级语言的语法结构转换成较低级的表示形式,以便后续的优化和目标代码生成。

    在Clang编译前端编译代码过程中, 中间代码的形式为 LLVM IR的形式输出的.

    我们使用如下终端指令, 来生成 LLVM IR 形式的中间代码.

    clang -S -emit-llvm helloworld.c -o helloworld.ll
    

    生成的中间代码如下所示.

    ; ModuleID = 'helloworld.c'
    source_filename = "helloworld.c"
    target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
    target triple = "x86_64-apple-macosx13.0.0"
    
    @a = global i32 7, align 4
    
    !llvm.module.flags = !{!0, !1, !2, !3, !4}
    !llvm.ident = !{!5}
    
    !0 = !{i32 2, !"SDK Version", [2 x i32] [i32 13, i32 3]}
    !1 = !{i32 1, !"wchar_size", i32 4}
    !2 = !{i32 7, !"PIC Level", i32 2}
    !3 = !{i32 7, !"uwtable", i32 2}
    !4 = !{i32 7, !"frame-pointer", i32 2}
    !5 = !{!"Apple clang version 14.0.3 (clang-1403.0.22.14.1)"}
    

    我们发现全局变量 int a 的结果已经被计算出来了, 如下所示.

    @a = global i32 7, align 4
    

  • 代码优化(Code Optimization)

    对中间代码进行优化,以提高程序的性能和效率。代码优化器负责分析和优化中间代码,如常量折叠、循环展开、函数内联等优化技术,以减少执行时间和内存占用。

    我们可以通过 -O 参数来优化生成的中间代码.

    clang -O3 -S -emit-llvm helloworld.c -o helloworld.ll
    

    这时候, 我们会得到如下的中间代码.

    ; ModuleID = 'helloworld.c'
    source_filename = "helloworld.c"
    target datalayout = "e-m:o-p270:32:32-p271:32:32-p272:64:64-i64:64-f80:128-n8:16:32:64-S128"
    target triple = "x86_64-apple-macosx13.0.0"
    
    @a = local_unnamed_addr global i32 7, align 4
    
    !llvm.module.flags = !{!0, !1, !2, !3, !4}
    !llvm.ident = !{!5}
    
    !0 = !{i32 2, !"SDK Version", [2 x i32] [i32 13, i32 3]}
    !1 = !{i32 1, !"wchar_size", i32 4}
    !2 = !{i32 7, !"PIC Level", i32 2}
    !3 = !{i32 7, !"uwtable", i32 2}
    !4 = !{i32 7, !"frame-pointer", i32 2}
    !5 = !{!"Apple clang version 14.0.3 (clang-1403.0.22.14.1)"}
    

    那么 -O3 是什么含义呢? 下面, 我整理了 Clang 遍历过程中所有的优化参数的具体定义.

    • 优化级别 O0 :这是最低级别的优化,默认情况下会开启。在这个级别下,编译器将进行最少的优化,以保持代码的简单性和可读性。

    • 优化级别 O1 :这个级别会应用一些轻量级的优化,以提高代码的执行速度和空间效率。编译时间通常较短,但优化效果有限。

    • 优化级别 O2 :这是默认的优化级别。它会开启更多的优化选项,包括内联函数、循环展开、变量替代等,以提高代码的执行速度。编译时间可能会比 O1 长一些。

    • 优化级别 O3 :这个级别会比 O2 应用更多的优化,但可能会导致编译时间显著增加。它会更加注重代码的性能优化,可能会牺牲一些可读性和可维护性。

    • 优化级别 Os :这个级别的优化旨在减小生成的可执行文件的大小,而不是提高执行速度。它可以通过减小代码的体积来节省内存和存储空间。

    • 优化级别 Oz :这是一个综合了优化级别 O2 和优化级别 Os 的级别。它会尽量减小代码的体积,并使用更多的性能优化。


    同时, 在 Xcode 编译项目工程时, 我们也是可以通过 Build SettingsOptimization Level 调整中间代码的优化级别.


    上述步骤就是编译前端做的工作, 下面来说一下编译后端都做了哪些工作.


  • 目标代码生成(Code Generation)

    将优化后的中间代码转换为目标机器代码(可执行代码)。代码生成器负责将中间代码转换为目标机器的指令序列,包括寄存器分配、指令选择和指令调度等。

    对于iOS来说, 我们主要根据不同架构的CPU转换成汇编代码, 然后再生成对应的可执行文件. 这样CPU就可以执行了.

    生成汇编代码, 我们主要利用到以下指令

    clang -S -o - helloworld.c | open -f 
    

    这时候, 汇编代码生成结果如下所示.

        .section	__TEXT,__text,regular,pure_instructions
        .build_version macos, 13, 0	sdk_version 13, 3
        .section	__DATA,__data
        .globl	_a                              ## @a
        .p2align	2
    _a:
        .long	7                               ## 0x7
    
    .subsections_via_symbols
    

  • 可执行文件的生成(Create Mach-O File)

    这一个步骤应该算在 目标代码生成 的一部分, 在生成目标平台的机器代码后,Clang 还需要进行链接操作,将生成的机器代码与所需的库文件和其他依赖项进行链接,以生成最终的 Mach-O 可执行文件。

    注: 由于 helloworld.c 中的内容为 int a = 1 + 3 * 2; , 并不能执行, 所以我们要在 helloworld.c 写一个完整的C语言main函数. 如下所示.

    #include <stdio.h>
    int main(int argc, char *argv[])
    {
        printf("Hello World!\n");
        return 0;
    }
    

    我们可以通过下面终端指令生成 Mach-O 文件.

    clang helloworld.c -o helloworld.out
    

    然后, 我们可以在终端使用 ./helloworld.out 正常调用Mach-O文件了.

    shenjingsaodong@Mac Desktop % ./helloworld.out 
    
    Hello World!
    

    然后, 我们可以通过 otool 工具可以查看生成的可执行文件的 sectionsegment, 这里我就不懂了… GG

    Segment __PAGEZERO: 0x100000000 (zero fill)  (vmaddr 0x0 fileoff 0)
    Segment __TEXT: 0x4000 (vmaddr 0x100000000 fileoff 0)
        Section __text: 0x2c (addr 0x100003f70 offset 16240)
        Section __stubs: 0x6 (addr 0x100003f9c offset 16284)
        Section __cstring: 0xe (addr 0x100003fa2 offset 16290)
        Section __unwind_info: 0x48 (addr 0x100003fb0 offset 16304)
        total 0x88
    Segment __DATA_CONST: 0x4000 (vmaddr 0x100004000 fileoff 16384)
        Section __got: 0x8 (addr 0x100004000 offset 16384)
        total 0x8
    Segment __LINKEDIT: 0x4000 (vmaddr 0x100008000 fileoff 32768)
    total 0x10000c000
    

  • 符号表管理(Symbol Table Management)

    维护符号表,记录程序中定义的变量、函数、常量等符号的信息。符号表包括符号名称、类型、作用域等信息,用于语义分析和代码生成阶段的符号查找和类型检查。

    当在编译过程中发生错误时, 我们是可以利用 符号表 来定位到报错位置在源文件中的位置的.

    通过, 我们在项目上线后, 当线上发生事故异常时, 我们也是可以利用 符号表 来确定问题所在的.

    基于Clang编译器,符号表管理的过程如下:

    • 符号表的组织

      Clang使用哈希表、树等数据结构来实现符号表。哈希表可以提供快速的查找和插入操作,树结构可以支持符号作用域的嵌套和查找。

    • 符号的添加

      在词法分析和语法分析过程中,当遇到变量、函数、类型定义等符号的声明和定义时,Clang将根据符号的属性创建一个符号表项,并将其添加到符号表中。符号表项包括符号名称、类型、作用域、存储位置等信息。

    • 符号的查找

      在进行语义分析和代码生成时,Clang需要查找符号表来获取符号的属性信息,如类型、存储位置等。通过符号名称和作用域,Clang可以在符号表中快速定位到符号表项,以提供相应的属性信息。

    • 符号的作用域

      符号表管理也涉及符号的作用域管理。Clang会维护一个作用域栈来跟踪当前有效的作用域。当进入一个新的作用域时,例如函数、循环或块作用域,Clang会将该作用域压入作用域栈中,并将内部的符号添加到符号表中。当离开作用域时,Clang会将该作用域从作用域栈中弹出。

    • 符号的重定义和重复定义检查

      Clang会检查符号表中是否存在重复定义或重定义的符号。如果发现重定义或重复定义的符号,Clang会生成相应的错误信息。


  • 错误管理(Error Management)

    在编译过程难免会遇到编译错误, 这时候, 出错管理体现着无与伦比的作用, 它帮助程序员发现和解决代码中的错误. 提高代码的可靠性和质量。在编译器的实现中,错误管理需要综合考虑准确性、恢复能力和用户友好性,以提供有效的错误处理和提示信息。

    以下是编译过程中的错误管理部分:

    • 错误检测(Error Detection)

      编译器在进行词法分析、语法分析、语义分析等阶段会检测代码中的语法错误、类型错误和其他语义错误。一旦检测到错误,编译器会生成相应的错误消息。

    • 错误报告(Error Reporting)

      编译器在检测到错误后,会将错误信息记录下来,并向用户报告。错误报告通常包括错误类型、错误位置和错误描述等信息,以帮助程序员找出和解决错误。

    • 错误恢复(Error Recovery)

      当编译器遇到错误后,可以尝试进行错误恢复来继续编译。错误恢复策略可以包括跳过错误部分、补全缺失部分、重新同步语法等,以尽可能提供更多的错误信息和继续编译的机会。


总结

这一篇博客还是比较偏向于理论知识的, 下一篇博客, 我们就着 词法分析 语法分析 语义分析 中间代码生成 这几个前端过程来构建我们的 PrattParser 解释器. 如果有任何问题, 欢迎留言. 欢迎持续关注骚栋.


点击查看 基于Swift的PrattParser项目


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/1104140.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

76.C++ STL list容器

目录 1.什么是list容器 2.list构造函数 3. 元素插⼊和删除操作 4.大小操作 5.赋值操作 6.数据存取操作 7.反转、排序 1.什么是list容器 list 是 C 标准库提供的双向链表容器。它与 vector 和 deque 不同&#xff0c;不是连续的内存块&#xff0c;而是由节点组成的链表结…

C语言——二周目——数据在内存中的存储

目录 一、整数的存储方式 二、浮点数的存储方式 一、整数的存储方式 因为CPU只有加法器&#xff0c;所以对于整型来说&#xff0c;数据在内存中通常采用补码的方式进行储存。 在这里复习一下原码、反码、补码。 正数和无符号数的原码、反码、补码相同&#xff1b; 负数的原…

考察软件开发公司的能力

当公司需要与软件外包公司合作时需要考察软件开发公司的能力和水平&#xff0c;这会涉及到很多方面的因素。需要通过综合考察和了解软件开发公司的能力和水平&#xff0c;选择合适的合作伙伴&#xff0c;确保项目的成功交付。下面分享一些关键步骤和方法&#xff0c;希望对大家…

【JVM】JVM的垃圾回收机制

JVM的垃圾回收机制 对象死亡判断方法引用计数算法可达性分析算法 垃圾回收算法标记清除法复制算法标记整理算法分代算法 Java运行时内存的各个区域,对于程序计数器,虚拟机栈,本地方法栈这三个部分区域而言,其生命周期与相关线程有关,随线程而生,随线程而灭,并且这三个区域的内存…

Excel冻结窗格

1、冻结表格首行 点击菜单栏中的“视图”&#xff0c;选择“窗口”选项卡中的“冻结窗格”下的小三角&#xff0c;再选择“冻结首行”&#xff1b; 2.冻结表格首列 点击菜单栏中的“视图”&#xff0c;选择“窗口”选项卡中的“冻结窗格”下的小三角&#xff0c;再选择“冻结…

图扑智慧仓储数据可视化监控平台

随着市场竞争加剧和市场需求的不断提高&#xff0c;企业亟需更加高效、智能且可靠的仓储物流管理方式&#xff0c;以提升企业的物流效率&#xff0c;减少其输出成本&#xff0c;有效应对市场上的变化和挑战。 图扑软件应用自研 HT for Web 产品搭建的 2D 智慧仓储可视化平台&a…

【轻松搞定】Edge 或 Google 无法上网问题

目录 前言 一、解决 Edge 无法上网的问题 1.1 键盘按下 WIN R 或 右键开始标志&#xff0c;启动运行 1.2 输入 regedit 进入注册表管理 1.3 打开到 \HKEY_LOCAL_MACHINE\SOFTWARE\Policies\Microsoft 位置下新建项 Edge 1.4 右键 Edge 新建 DWORD (32) 文件&#xff0c…

<FPGA>好的编码风格(1)--尽量避免组合逻辑环路(Combinational Loops)

什么是组合逻辑环路&#xff1f; 组合逻辑环路&#xff08;Combinational Loops&#xff09;&#xff1a;指组合逻辑的输出信号不经过任何时序逻辑&#xff08;FF等&#xff09;&#xff0c;而是直接反馈到输入节点&#xff0c;从而构成的电路环路。 此外&#xff0c;如果直接将…

【开源分享】基于Html开发的房贷计算器,模仿新浪财经

房贷计算器是一种房贷计算的在线计算Web应用&#xff0c;按用户选择的贷款类型、贷款金额、期限、利率可计算得出每月月供参考、支付利息、还款总额这些信息。本文模仿新浪财经开发的房贷计算器。 作品预览 https://fangdai.gitapp.cn 源码地址 https://github.com/geeeeeee…

Qt5.12.12构建64位QMYSQL数据库驱动“driver not loaded”

在调用QSqlDatabase::open()时,会报错:“driver not loaded” 原因实际上是mysql 的驱动 qsqlmysql.dll 没有成功加载。 所以本篇文章将详细介绍一下:Qt5.12.12如何构建64位QMYSQL数据库驱动。 执行 写在最前,以下出现的文件路径为我自己电脑安装的路径,可根据自己的路…

vim快捷指令

普通模式—>插入模式 i:插入到当前光标的前面&#xff08;insert&#xff09; a:插入到光标的后面&#xff08;append&#xff09; o:插入到下一行 I:插入到行首 A:插入到行尾 O&#xff1a;插入到前一行 H:行首 L:页的最后一行的行首 W:光标跳到下一个单词词首 b:上一个单…

广州华锐互动:VR模拟高楼层建筑应急逃生,提供身临其境的虚拟体验

随着城市化进程的不断加速&#xff0c;高层建筑越来越多地出现在我们的生活中。然而&#xff0c;高层建筑的安全问题也日益凸显。一旦发生火灾、地震等突发事件&#xff0c;如何迅速、安全地逃离高楼成为了人们关注的焦点。近年来&#xff0c;虚拟现实&#xff08;VR&#xff0…

JUC并发编程——ForkJoin与异步回调

ForkJoin &#xff08;分支合并&#xff09; 什么是ForkJoin ForkJoin在JDK1.7出现 &#xff0c;并行执行任务&#xff0c;在大数据量下&#xff0c;能够提高效率 讯飞星火提供的说法&#xff1a; Forkjoin是一种并行计算的算法&#xff0c;用于将一个大任务分解为多个小任务…

【PSO-RFR预测】基于粒子群算法优化随机森林回归预测研究(Matlab代码实现)

&#x1f4a5;&#x1f4a5;&#x1f49e;&#x1f49e;欢迎来到本博客❤️❤️&#x1f4a5;&#x1f4a5; &#x1f3c6;博主优势&#xff1a;&#x1f31e;&#x1f31e;&#x1f31e;博客内容尽量做到思维缜密&#xff0c;逻辑清晰&#xff0c;为了方便读者。 ⛳️座右铭&a…

MinIO (三) 使用Webhook实时同步文件

前言 使用MinIO的过程中&#xff0c;我们这里遇到个需求&#xff0c;系统需要预览3D文件&#xff0c;前端操作&#xff0c;用浏览3D文件的工具打开3D文件的目录即可实现在线预览功能&#xff0c;这个时候问题来了&#xff0c;MinIO上传文件后进行了编译&#xff0c;如下图 这个…

ardupilot开发 --- 外设适配器、拓展外设、AP_Periph、DroneCAN 、UAVCAN 篇

什么是外设适配器&#xff0c;为什么要拓展外设&#xff1f; 飞控的外设接口有限&#xff0c;如串口只有8个&#xff0c;I^2C只有一个&#xff0c;CAN口只有一个&#xff0c;这些接口数量如果不能满足预期的传感器数量&#xff0c;那么就需要对这些接口进行拓展&#xff0c;外…

ELK日志分析系统的详细介绍与部署

文章目录 1. ELK的概述1.1 简介1.2 使用ELK的理由1.3 ELK的主要组件1.3.1 Elasticsearch1.3.2 Kibana1.3.3 Logstash1.3.3.1 简介1.3.3.2 Logstash常用相关命令选项 1.3.3.3 Logstash 的输入和输出流1.3.4 Logstash的相关配置文件 1.3.4 Filebeat1.3.4.1 简介1.3.4.2 filebeat …

【vue2高德地图api】01-创建应用,获取key值

系列文章目录 【vue2高德地图api】视频效果&#xff08;手机端&#xff09;先看这里 文章目录 系列文章目录前言创建key&#xff08;2个&#xff09;1.1进入控制台1.2进入应用1.3 创建应用1.4输入名称和类型2.1 添加key2.2 选择对应信息2.3 创建js key和服务端 key 总结 前言 …

数仓建设(一)

想了想&#xff0c;我们的数仓的建设是基于大数据平台进行的&#xff0c;中间也经历了比较曲折的过程。 每个行业都有自身的业务区别&#xff0c;不过很多还是比较相通的。 本文将全面讲解数仓建设规范&#xff0c;从数据模型规范&#xff0c;到数仓公共规范&#xff0c;数仓各…

vue3实现el-card的body的样式穿透

:deep(.el-card .el-card__body){padding-top:20px!important; }改el-card的body 将内容的padding变成上下左右都是20px el-card默认是上面为15px,其他左右下都是20px 详解源码vue3的样式穿透scope-CSDN博客