




已阅读5页,还剩37页未读, 继续免费阅读
版权说明:本文档由用户提供并上传,收益归属内容提供方,若内容存在侵权,请进行举报或认领
文档简介
类C微小编译器的设计与实现摘要 随着计算机的广泛应用,计算机编程语言从早期的机器语言到汇编语言进行不断地演进,以及到现在的各种高级语言的形态。 编译器技术是计算机技术发展的基石,同时也是进展速度最快的计算机科学,这个分支已经进行了几十年的研究,形成了非常成熟的体系,编译器的设计集中体现了计算机的本质和发展的成果。 其核心思想是在机器语言和算法的逻辑结构转换从一种基础到另一种代表语言的过程。最终形成高级别语言,甚至在高级语言上的虚拟平台上运行的机器语言,并且以硬件的机器指令实现,以上所述的改造涉及到的编译器技术的应用。本系统采用Go作为编程语言。介绍了开发的相关内容,完成的功能,以及实现的记录。着重解释了一些编写编译器的关键点和技术要点和理论。关键词: 编译技术,编程程序,高级语言第一节 绪论1.1 开发背景 在计算机技术与科学的迅猛发展下, 计算技术应用在了非常广泛的领域当中, 相当的计算机应用层出不穷,极大地丰富了我们的生活,学习和工作。与此同时, 也有许多用于编写高级应用的编程语言作为支撑,才得以构建非常复杂的系统和架构。 程序设计是一门艺术,设计者通过特定语言的编译器将源代码翻译为体系结构相关的目标程序,,从而能够最终达到程序执行的目的,,一个好的程序语言要满足工程的需要也要搭配良好的语法设计。从 20 世纪 60 年代开始,编译器的设计就是计算机科学技术发展和研发中的一个热门主题。虽然编译器设计的历史很长,作为计算机技术来说相对成熟,编译器还是一个由高级抽象的源代码向计算机机器指令转换的高效映射工具,随着电脑软硬件水平的不断提高,编译器的设计也在不断地变化,目标机体系结构也在不断地改进,软件越来越复杂,其规模也越来越大。编译器设计问题在高级层次上大致稳定,例如面向对象语言,面向过程语言,函数语言都在各自的领域发挥着重要的作用,并且每个领域的研究都有着非常积极的意义,不同领域相互学习,相互弥补,并且结合工业界的积累,不断自我完善,从一个抽象的计算机科学问题,逐渐变成了一个计算机工业界不断追求和寻找优化的目标,大公司对于编程语言的要求非常高,因为一个良好的编程语言能很大的提高程序员的工作效率,良好的程序组织结构也能保证项目的可重用性,这样实际生产中的工程能够保证高效和稳定。当我们深入其内部研究编译器时会不断明白,编译器的内部结构同样也在顺应着需求一直在变化。此外,由于我们能够提供给编译器本身的计算资源,例如内存等,也在不断增加。所以,现代编译器可以采用比以前更加复杂的算法,却又不会损失过多的时间。除此之外,编译器前后端分离,能够提供一种更加合理的方式,把编程过程解耦,比如在编辑时就主动触发前端语法分析的过程,检查语法错误,而不需要等到最后编译的时候进行,或者通过解析条件自动生成代码,减少开发的工作量,很多编译“前端”技术,如文法、正则表达式、语法分析器以及语法制导翻译器等,仍然被广泛使用。1.2 开发的目标和意义 GCC的复杂程度基本上可以和Linux操作系统比肩,代码量,工程量非常之大,是世界各地的优秀工程师合力通过版本控制系统贡献开发的,单凭一个人很难做到这样规模的编译器,所以编写甚至读懂这样的一个GCC的编译器是一件非常困难的事情。很多的计算机专业的同学从来没有编写过一个完整的编译器,但是,几乎任何程序都和编译器有关,而且任何一个软件工程师都应该知晓编译技术的基本结构和原理。编译设计的原理和技术还可以用于编译器设计之外的众多领域。因此,这些原理和技术会在工程实践中被反复使用。研究编译器的实现和设计程序语言、体系结构、算法和软件工程。编译器的设计从本质上来说是一项工程,它所使用的方法必须很好地解决现实中出现的各种解释逻辑(即用真实的语言编译且在真实的机器上能够执行的程序)。在一般情况下,开发编译器的人必须面对机器结构,很少能够去改善设计。在开发过程中做什么样的分析和转换,以及什么时候去做,这些都是工程上的选择,但正是这些选择决定了一个编译器的性能高低。本实验就建立在一个自主开发的微型编译器基础之上的,该编译器虽然功能类似于C语言这样的经典编译器,但是缺少必要的标准库和完整的实现,但也已经完全具备了一个编译器应有的所有特征。虽然本实验只是一个规模很小的微型编译器的开发,作为一次较为完整的编译开发实践,它已经足够透彻地了解一个编译器开发过程,又能更深刻地理解和运用编译开发过程中的众多技术和方法,并能在此基础上针对编译器的优化展开深入的讨论,这些对于自己以后的研究和发展方向将起到非常大的推动作用。1.3 编译原理的发展情况 在编译器原理的进化历程里,提高编译的效率始终是编译器专家挖掘的领域之一,编译效率指的是根据编译器产生的目标代码的时间指标和空间指标的代价决定的效率,所以编译优化要围绕时间和空间这两个方面来实施。在编译时针对过程进行优化的技术有很多,它们通常是通过搜集源代码或中间代码的特定信息,然后利用这些元素对代码中的源代码组织结构或算法模型等实施等价的改进变换,从而致力于在时间效率和空间效率上达到一个比较理想的效果。编译器的设计者们一直想要能够将各种代码优化技术充分地运用在自己的语言设计中,但往往并不是尽如人意。本编译器开发过程中,虽然没有运用到非常优化的代码,但通过本实验的进行,在现有开发的编译器基础之上,能够在后续的开发中逐渐地提高程序的编译效率,对于自己以后的研究和发展方向将起到充分的促进效果。这正是本实验的研究意义所在。本实验是以微型编译器的项目开发为基础,该项目的开发目标是自定义一种高级语言,然后编码实现出语言的编译器,完成将语言源程序翻译为基于虚拟机的目标代码的任务,这是这个实验的应用目标。编程语言的开发具有极高的工程价值和应用意义,高级语言编译器的性能决定了基于该语言应用所能表现的质量。所以国际上很多的科研和技术人员也在积极地开展这方面的技术探索和项目实践。大多是以特定的工程项目为前提来进行一些与编程语言设计相关或类似的项目分析,研究目标大多是基于某种实验型高级语言的编译器开发和优化改进,然后把有价值的研究成果移植或运用到工业级级的编译器开发中,最近十年以来,国外关于编译器设计的发展动态主要体现在:编译器采用了更加复杂的算法,主要用于推演或压缩程序中的元信息,这又与更为复杂的程序设计语言的发展结合在一起,其中典型的有用于函数语言编译的 Hindley-Milner 类型检查的统一算法 (!这里有个引用原文2的引用) 。 在九十年代,作为GNU项目或其它开放源代码项目的子项目GCC为基础,许多开源的编译器或构造工具被发布了。这些工具可用来编译程序语言的源程序。它们中的一些项目被认为是非常优秀的,而且对现代编译理论感兴趣的人都可以轻松地得到它们的免费源代码。比如Clang2设计时比GCC编译过程中保留更多的信息,保留原始代码的整体形式。这样做的目的是使其更容易将映射误差引入原始源。通过Clang提供的错误报告的目的还在于要更加详细和具体的错误提示,所以可以在IDE编译参数的输出中体现。编译器的模块化设计可以提供源代码索引,语法检查,以及其他功能通常与快速应用开发系统相关。解析树也更适合于自动化配套代码重构,因为它直接代表着原始的源代码。第二节 理论基础2.1.1 编译器系统概述 编译器是一个计算机程序(或一组程序),从编写的编程语言(源语言)到另一个计算机语言(目标语言)的源代码的转化程序,而后者往往具有称为对象代码二进制形式。“编译”主要用于从一个高级语言到较低水平的语言翻译源代码的程序(例如,汇编语言或机器代码)。如果编译程序可以在不同CPU或操作系统上编译运行,则编译器被称为跨平台的编译。编译器从某种程度上说是一种翻译器。虽然这个过程这需要一套编程规范,并翻译它们,即所有程序创建的目标来执行这些中准则,在技术上“编译”意味着,从编译器生成一个独立的可执行程序(即可能需要运行时间库或子系统来支持的程序),如果仅执行原规格编译器通常被称作“解释器”,因为不同的分析对于编译和解释之间是有区别的,在这两个词之间有一些重叠的方法,但是编译更倾向于生成二进制文件而解析则仅仅是对源代码进行翻译并且进行动态执行。 从低水平的语言向更高一级翻译的程序是反汇编。高层次的语言之间转换的程序通常被称为一个源到源的编译器。语言重写通常是翻译表达的形式。编译器编译有时用来指一个解析器生成器,经常被用来帮助创建词法和语法分析器的工具。编译器很可能要进行以下操作:词法分析,预处理,解析,语义分析(语法制导翻译),代码生成和代码优化。2.1.2 编译器的产生历史 早期计算机的软件主要是用汇编语言编写。虽然第一个高级语言产生时用着几乎一样古老的电脑,导致了在设计编译器时最大的技术挑战是早期的计算机有限的内存容量。第一个高级语言(Plankalkl)于1943年提出,是康拉德楚泽发明,在1952年,则为A-0编程语言;在A-0运作更象是一个装载机或连接器,不太接近现代的编译器。第一个编译器被阿利克Glennie于1952年实现了,当时在曼彻斯特大学的一台电脑上,被一些人认为是第一个编译的编程语言。IBM由约翰巴科斯领导的FORTRAN团队一般被作为在1957年诞生的COBOL的第一个完整的编译器的作者,该编译器能够在多个架构上进行编译,在1960年在许多应用领域使用更高级别语言的想法很快就流行起来。因为新的编程语言支持的扩展功能和计算机体系结构的日益复杂,编译器变得更加复杂。早期的编译器是用汇编语言编写。第一个自举的编译器 - 能够编译自己的源代码的语言 - 是在1962年由蒂姆哈特和麦克莱文在麻省理工学院,用Lisp创建的。自1970年以来这已成为实现编译器一个常见的做法。虽然两者Pascal和C一直是实现语言流行的选择的语言。构建自举的编译器是一个非常重要的问题,第一个这样的编译器的语言必须通过用其他语言实现最初版本的编译器。2.2 编译器的结构 高级语言编译器是源程序与底层硬件交互的桥梁。编译器验证代码语法,生成高效的目标代码,运行时的组织,并格式化根据汇编器和链接约定的输出。编译器包括: 前端:验证的语法和语义,以及由中间端产生用于处理的源代码的中间表示。执行信息收集对类型进行检查,产生错误和警告。前端的方面包括词法分析,语法分析和语义分析。 中间端:进行优化,包括去除无用的或无法访问的代码,发现常量传播,计算迁移不太频繁执行的地方(例如,跳出了一条循环)。 后端:生成汇编代码,在执行过程中进行寄存器分配。梳理如何并行执行以及整理执行单元,优化硬件的目标代码的应用(对于程序变量在可能的情况下分配处理器寄存器)。2.3.1 编译器的组织 在早期,编译器采取的设计方法复杂性受过往经验和消耗资源的影响。这种通过一个人写一个比较简单的语言编译器可能是一个单一的,整体的软件。当源语言变得庞大而复杂后,高品质的输出是必需的,该设计可被分成若干相对独立的模块。具有单独的阶段意味着开发任务可以被分配成小部分,并给予不同的人来完成。通过改进替换的独立的模块也变得容易得多。编译过程阶段的划分是由生产质量编译器编译器项目(PQCC)在卡内基 - 梅隆大学的拥护。该项目推出了前端,中端和后端的划分。一般之后有前后端。然而,中间端通常被认为是前端或后端的一部分。在这两个平衡点之间取舍是公开议题。前端通常被认为在句法和语义处理发生时进行的处理,与翻译一起表示的较低的水平(低于源代码)。 中间端通常被设计为比源代码或机器代码以外的形式执行并优化。与源代码/机器代码独立,目的是使能够支持不同的语言和目标处理器的编译器版本之间共享通用的优化。后端需要从中间端输出。它可以执行的是针对特定计算机进行的更多的分析,转换和优化。然后,它为特定的处理器和操作系统生成代码。该前端/中间/后端的方法使得有可能结合用于与不同语言前端为不同的CPU绑定特定汇编语言。这种方法的实际例子是GNU编译器,LLVM3,其中有多个前端和多种后端。 通过分遍次数分类的编译器有其计算机的硬件资源有限的原因。编译涉及执行大量的工作和早期计算机没有足够的内存来包含所有做这项工作的一个程序。所以编译器被分裂成执行某些独立子功能的分析和翻译的方案,其中每个由前一个传过来的源(或它的某种表示)作为输入。在单次通过编译的能力已被看作是一个有益的过程,简化了的编译器和一个通用编译器通常执行编译比多遍编译器更快的工作。因此,在早期系统的资源限制带动下,许多早期的语言是专门设计,使他们能够在同一路径上进行编译。在一些情况下,语言功能的设计可能需要编译器执行一个以上的传过来的源。例如,声明出现在其引用出现的后面。在这种情况下,语句的翻译源代码,第一遍需要收集有关报表后出现的申明信息,他们的影响,与实际发生的翻译在随后的遍历中通过。单次通过编译的缺点是它不可能执行许多高质量的代码所需要的复杂的优化。它难以控制优化编译器。举例来说,优化的不同阶段可以分析一个表达很多次,但只有一次分析生成另一种表达。拆分编译成小的程序是生产可证明正确的编译器研究人员使用的技术。证明了一组小程序的正确性往往需要比证明一个更大的,单一的程序的正确性更省力。而典型的多遍编译器从它的最终道输出机器代码,还有其他几种类型:“源到源的编译器”是一种编译器,需要一个高级语言作为其输入和输出语言。例如,自动并行化编译器会经常把一个高级语言程序作为输入,然后转换代码和并行代码的注释或语言结构对其进行批注。编译器编译为一个理论机器的汇编语言,像一些虚拟机的实现,针对Java,Python和更多的字节码编译器也都是这样的类型。应用程序在字节码上的编译是编译为机器码在执行之前的隔离层。2.3.2 编译器前端 编译器前端通过分析源代码来构建程序,称为中间表示。它还管理符号表,数据结构在源代码相关的映射信息,如位置,类型和符号。而前端可以是一个单一的整体功能或程序,如在无扫描的解析器中,只要主要实现分析的几个阶段,其可以顺序地或并发执行。特别是对于良好的工程来说:模块化和关注点分离。词法上,语法分析和语义分析。文法的解析包括句法分析(分别为记号的语法和表达式的语法),并在一半情况下,这些模块(词法分析器和解析器)可从该语言语法自动生成,但在更复杂的情况下,这些需要手动修改或手写。词法文法和短语语法通常上下文无关文法,能显著简化分析,在语义分析阶段处理上下文的灵敏度上更有优势。语义分析阶段一般是更复杂的,并用手写的,但可使用语法被部分或完全自动化。这些阶段本身可以进一步细分 - 词法扫描和评估,作为解析首先建立一个具体语法树(解析树),然后将其转变成一个抽象语法树。 在一些情况下有附加的阶段被使用,特别是线性重建和预处理的时候,但这些比较少见的。可能的阶段的详细清单,包括: 行改造,关键字或允许任意空间内标识需要分析的预处理阶段,输入字符序列转换为规范的形式准备提供给解析器语言。自上而下,递归下降,在20世纪60年代使用的表驱动的解析器通常一次读取源一个字符,并不需要一个单独的标记化阶段。 词法分析分割了源代码文本并切成独立的词法记号。每个记号是语言的单个原子单位,比如关键字,标识符或符号名称。记号语法是典型的常规语言,所以来自正则表达式构成的有限状态自动机可以被用来识别它。此阶段也被称为词法分析或扫描过程,而软件操作的词法分析被称为词分器或扫描器。这可能不是一个单独的步骤 - 它可以在解析步骤中无扫描解析,在这种情况下的解析是在字符级别,而不是记号级别进行组合的。 有些语言,像C一样需要支持宏替换和条件编译预处理阶段。通常情况下,预处理阶段之前,语法和语义分析就会发生,预处理器操纵词法标记,而不是语法形式。然而,有些语言,支持基于语法形式宏替换。 语法分析包括解析词法记号序列来识别程序的语法结构。此阶段通常生成解析树,根据一个正式的语法限定的语言规则建立的树结构替换记号的线性序列。解析树通常由以后的阶段改变。 语义分析是编译器增加了语义信息来解析解析树并建立符号表的位置。此阶段进行语义检查,如类型检查(检查类型错误),或对象绑定(变量和函数引用与他们的定义关联),或明确赋值(要求所有本地变量在使用前进行初始化),拒绝不正确的代码或发出警告。语义分析通常需要一个完整的分析树,这意味着该逻辑下解析阶段的过程中,在逻辑上早于代码生成阶段,虽然它通常可以将多个过程折叠成一个通过在一编译器实现的代码。2.3.3 编译器后端 编译器后端这个术语常常和生成汇编代码的功能重叠,有时与代码生成器混淆。所以使用中端区分通用的分析和优化阶段和机器相关的代码生成器的后端。 后端的主要阶段包括如下几点: 分析:这是根据从输入的中间表示信息进行收集的过程,数据流分析是用来建立定义的工具链的,具有相关性分析,别名分析,指针分析,逃逸分析等精确分析,为编译器优化的基础。调用图和控制流图也常常在分析阶段建成。 优化:中间语言表示被变换成功,并转化成速度更快的形式。大多数优化是内联展开,无效代码消除,常量传播,循环转换,寄存器分配,甚至自动并行化等。 代码生成:转化的中间语言被翻译成输出语言,通常是该系统的本机语言。这涉及到资源和存储决策,比如决定以适合哪些变量及其相关寻址模式以及寄存器和适当的机器指令和内存的选择和调度。调试数据也可能需要产生。 编译器分析为任何编译器优化的前提,之间紧密地进行工作。例如,相关分析是循环转换的关键。此外,编译器分析优化的范围相差很大,从小到基本块的程序/功能水平,甚至在整个程序(间优化)。编译器可能会做用更广阔的工作。但是,丰富的功能是不是无条件的:大范围的分析和优化是在编译时间和存储空间方面造成昂贵的开销,分析和优化尤其如此。2.3.4 编译器错误处理 编译器错误处理单独提出的一个很重要的原因是,在编译器设计的过程中,错误处理是一个很容易忽视的过程,因为大部分编程语言的错误提示并不友好。编译器的正确性是软件工程与一个编译器语言规范行为的一个分支。包括使用形式化方法开发编译器和使用现有的编译器进行严格的测试(通常被称为编译器验证)。第三节 编译器的实现3.1 语言定义标准运算符基本上的操作符都支持,且操作符优先级相同。包括:* +、-、*、/、%、=* 、&、&、|、* +=、-=、*=、/=、%=、+、-* =、&=、&=、|=、=* !、=、=、!=、&、|* - 类型原理上支持所有的类型。典型的有:* 基本类型:int、float、string、byte、bool、var(类似于C的union)。* 复合类型:slice、map、chan* 用户自定义:函数(闭包)、类成员函数、类常量* 布尔类型:true、false(由 builtin 模块支持)* var 类型:nil(由 builtin 模块支持)* 浮点类型:pi、e、phi (由 math 模块支持)变量及初始化基本的有a = 1 / 创建一个 int 类型变量,并初始化为 1b = hello / string 类型c = true / bool 类型d = 1.0 / float 类型e = h / byte 类型string 类型a = hello, worldstring有如下内置操作:a = hello + world / + 为字符串连接操作符n = len(a) / 取 a 字符串的长度b = a1 / 取 a 字符串的某个字符,得到的 b 是 byte 类型c = a1:4 / 取子字符串slice 类型a = 1, 2, 3 / 创建一个 int slice,并初始化为 1, 2, 3b = 1, 2.3, 5 / 创建一个 float slicec = a, b, c / 创建一个 string sliced = a, 1, 2.3 / 创建一个 var slice (等价于 Go 语言的 interface)e = mkslice(int, len, cap) / 创建一个 int slice,并将长度设置为 len,容量设置为 capf = mkslice(type(e), len, cap) / 创建一个 int slice 的 slice,也就是 Go 语言里面的 intslice有如下内置的操作:a = append(a, 4, 5, 6) / 含义与 Go 语言完全一致n = len(a) / 取 a 的元素个数m = cap(a) / 取 slice a 的容量b1 = b2 / 取 b 这个 slice 中 index=2 的元素b2 = 888 / 设置 b 这个 slice 中 index=2 的元素值为 888b1, b2, b3 = 777, 888, 999 / 设置 b 这个 slice 中 index=1, 2, 3 的三个元素值b2 = b1:4 / 取子slice特别地,可以这样赋值:x, y, z = 1, 2, 3结果是 x = 1, y = 2, z = 3。这是基础设计导致的:map 类型a = a: 1, b: 2, c: 3 / 得到 mapstringint 类型的对象b = a: 1, b, 2.3, c: 3 / 得到 mapstringfloat64 类型的对象c = 1: a, 2: b, 3: c / 得到 mapintstring 类型的对象d = a: hello, b: 2.0, c: true / 得到 mapstringinterface 类型的对象e = mkmap(string:int) / 创建一个空的 mapstringint 类型的对象f = mkmap(mapOf(string, type(e) / 创建一个 mapstringmapstringint 类型的对象map类型有如下操作n = len(a) / 取 a 的元素个数x = ab / 取 a map 中 key 为 b 的元素x = a.b / 含义同上,但如果 b 元素不存在会 panicae, af, ag = 4, 5, 6 / 同 Go 语言a.e, a.f, a.g = 4, 5, 6 / 含义同 ae, af, ag = 4, 5, 6delete(a, e) / 删除 a map 中的 e 元素需要注意的是,ab 的行为常见的范式是:x = a: 1, b: 2a = xa / 结果:a = 1if a != undefined / 判断a存在的逻辑.c = xc / 结果:c = undefined,注意不是0,也不是nild = xd / 结果:d = undefined,注意不是0,也不是nilchan 类型ch1 = mkchan(bool, 2) / 得到 buffer = 2 的 chan boolch2 = mkchan(int) / 得到 buffer = 0 的 chan intch3 = mkchan(mapOf(string, type(ch2) / 得到 buffer = 0 的 chan mapstringchan intchan 有如下内置的操作:n = len(ch1) / 取得chan当前的元素个数m = cap(ch1) / 取得chan的容量ch1 - true / 向chan发送一个值v = -ch1 / 从chan取出一个值close(ch1) / 关闭chan,被关闭的chan是不能写,但是还可以读(直到已经写入的值全部被取完为止)需要注意的是,在 chan 被关闭后,-ch 取得 undefined 值。所以应该这样:v = -ch1if v != undefined / 判断chan没有被关闭的逻辑.类型转换自动类型转换大部分情况下,不会自动类型转换。一些例外是:* 如果一个函数接受的是 float,但是传入的是 int,会进行自动类型转换。强制类型转换a = int(a) / 强制将 byte 类型转为 int 类型b = float(b) / 强制将 int 类型转为 float 类型c = string(a) / 强制将 byte 类型转为 string 类型流程控制if 语句if booleanExpr1 / . elif booleanExpr2 / . elif booleanExpr3 / . else / .switch 语句switch expr case expr1:/ .case expr2:/ .default:/ .或者:switch case booleanExpr1:/ .case booleanExpr2:/ .default:/ .for 语句for / 无限循环,需要在中间 break 或 return 结束.for booleanExpr / 类似很多语言的 while 循环.for initExpr; conditionExpr; stepExpr .典型例子:for i = 0; i 10; i+ .另外也支持 for.range 语法:for range collectionExpr / 其中 collectionExpr 可以是 slice, map 或 chan.for index = range collectionExpr .for index, value = range collectionExpr .函数函数和闭包基本语法:funcName = fn(arg1, arg2, argN) /.return expr这就定义了一个名为 funcName 的函数。本质上来说,函数只是和 1、hello 类似的一个值,只是值的类型是函数类型。可以在一个函数中引用外层函数的变量。如:x = fn(a) b = 1y = fn(t) return b + treturn y(a)println(x(3) / 结果为 4但是如果你直接修改外层变量会报错:x = fn(a) b = 1y = fn(t) b = t / 这里会抛出异常,因为不能确定你是想定义一个新的 b 变量,还是要修改外层 x 函数的 b 变量y(a)return b如果你想修改外层变量,需要先引用它,如下:x = fn(a) b = 1y = fn(t) b; b = t / 现在正常了,我们知道你要修改外层的 b 变量y(a)return bprintln(x(3) / 输出 3不定参数a = max(1.2, 3, 5, 6) / a 的值为 float 类型的 6b = max(1, 3, 5, 6) / b 的值为 int 类型的 6也可以自定义一个不定参数的函数,如:x = fn(fmt, args.) printf(fmt, args.)这样就得到了一个 x 函数,功能和内建的 printf 函数一模一样。多赋值x, y, z = 1, 2, 3.5a, b, c = fn() return 1, 2, 3.5 / 返回的是 float slice()需要注意的是,带上进行多赋值和不带进行多赋值在语义上有一点点不同。下面是例子:x1, y1, z1 = 1, 2, 3.5x2, y2, z2 = 1, 2, 3.5println(type(x1), type(x2)x1 的类型为 int,而 x2 的类型是 float。defer这在处理系统资源(如文件、锁等)释放场景非常有用。一个典型场景:f, err = os.open(fname)if err != nil / 做些出错处理returndefer f.close()/ 正常操作这个 f 文件类一个用户自定义类型的基本语法如下:Foo = class fn setAB(a, b) this.a, this.b = a, bfn getA() return this.a有了这个 class Foo,我们就可以创建 Foo 类型的 object 了:foo = new Foofoo.setAB(3, hello)a = foo.getA()println(a) / 输出 3构造函数构造函数只是一个名为 _init 的成员方法(method):Foo = class fn _init(a, b) this.a, this.b = a, b有了这个 class Foo 后,我们 new Foo 时就必须携带2个构造参数了:foo = new Foo(3, hello)println(foo.a) / 输出 3include语法一个文件可以通过 include 文法来将另一个文件的内容包含进来。所谓包含,其实际的能力类似于将代码拷贝粘贴过来。例如,在某个目录下有 a 和 b 两个文件。其中 a 内容如下:println(in script A)foo = fn() println(in func foo:, a, b)其中 b 内容如下:a = 1b = 2include a.qlprintln(in script B)foo()模块及 import模块(module)是一个目录,该目录下要求有一个名为 main 的文件。模块中的标识(ident)默认都是私有的。想要导出一个标识(ident),需要用 export 语法。例如:a = 1b = 2println(in script A)f = fn() println(in func foo:, a, b)export a, f这个模块导出了两个标识(ident):整型变量 a 和 函数 f。要引用这个模块,我们需要用 import 文法:import foo/bar.v1import foo/bar.v1 as bar2bar.a = 100 / 将 bar.a 值设置为 100println(bar.a, bar2.a) / bar.a, bar2.a 的值现在都是 100bar.f()将一个模块 import 多次并不会出现什么问题,事实上第二次导入不会发生什么,只是增加了一个别名。include 和 import 的区别include 是拷贝粘贴,比较适合用于模块内的内容组织。比如一个模块比较复杂,则可以用 include 语句分解到多个文件中。它永远基于 _dir_(即 include 代码所在脚本的目录) 来定位文件。import 是模块引用,适合用于作为业务分解的主要方式。反射在任何时候,你都可以用 type 函数来查看一个变量的实际类型,如:t1 = type(1)t2 = type(fn() )我们得到了 *Function。这说明尽管用户自定义的函数原型多样,即使定义多种多样但是类型是一致的。我们也可以看看用户自定义的类型:Foo = class fn f() t1 = type(Foo)t2 = type(Foo.f)foo = new Foot3 = type(foo)t4 = type(foo.f)可以看到,class Foo 的类型是 *Class,而 object foo 的类型是 *Object。而 Foo.f 和普通用户自定义函数一致,也是 *Function,但 foo.f 不一样,它是 *Method 类型。样例代码,求最大素数输入 n,求 n 的最大素数。用法:primes = 2, 3n = 1limit = 9isPrime = fn(v) for i = 0; i n; i+ if v % primesi = 0 return falsereturn truelistPrimes = fn(max) v = 5for for v = max returnv += 2v += 2n; n+limit = primesn * primesnmaxPrimeOf = fn(max) if max % 2 = 0 max-listPrimes(max)n; n = len(primes)for if isPrime(max) return maxmax -= 2/if len(os.args) 2 fprintln(os.stderr, Usage: maxprime )returnmax, err = strconv.parseInt(os.args1, 10, 64)if err != nil fprintln(os.stderr, err)return 1max-v = maxPrimeOf(max)println(v)3.2 词法分析 在计算机科学中,词法分析是字符(如在计算机程序或网页)的序列变换为标记(具有确定的字符串)序列的过程。这样的词法通常与一个分析器相关,它们分析的编程语言,网页的语法,等等结合。词法分析一般是编译器的第一部分,而且词法分析很简单,就是一个有限状态机。 开始词法分析的过程就是把源文件转换成一组预先定义好的token的过程。这一组被统一表现的token之后会被送入语法分析器进行语法解析,这里我们主要关注如何进行词法分析。 做词法分析就几种方法: 1. 直接使用工具比如lex。 2. 使用更低一层的正则表达式。 3. 使用状态动作,构造一个状态机。 而真正实现一个语言的话,使用工具没有什么错,但是问题是,很难获得正确的错误提示。工具生成的错误处理很弱,而且需要学习另一门规则或特定的语法,生成的代码可能性能不好,难以优化,但是用工具可以非常简单实现词法分析。早期编译器的设计阶段都会使用lex来做词法分析器,比如gcc就是这么做的,但是到了后期一个真正生产化的语言可能不能依赖生成的代码,而需要做自己特定的修改和优化,所以自己实现一个词法分析器就显得比较重要了。 正则表达被人诟病的一个话题就是效率问题,比如perl拥有功能最强大的正则表达式,但是整个正则表达式引擎的效率却很低,C在这方面牺牲了一些正则表达式的特性来保证正则表达式的效率不至于过低,但是正则表达式对于大量文本处理体现的弱势却是很明显的。因为可能我们要处理的状态其实不需要一个繁重的正则表达来解决。其实实现一个词法分析器很简单,而且这种技能是基本不会变的,如果写过一次,以后都是同样的实现方式。 词法记号是代表一个语义明确且表示其分类的结构。词法记号的类别的实例可以包括“识别符”和“整数文字”等,词法记号类别不同的编程语言会不同。从字符输入流形成记号的过程被称为记号化。比如说语句“sum = 3 + 2;”可以表示为,标识符sum+赋值符+整数3+符号+整数2+分号的一个记号流。本项目中使用的词法分析器是tpl,类似于yacc的一个词法分析器。TPL 全称是Text Processing Language(文本处理语言)。3.2.1 TPL实现原理3.2.1.1 token化首先要定义tokentype Token int其实就是个枚举类型,对于每种类型的字面值都有对应的token。实际上这个只能算是一个token的类型。首先枚举所有可以碰到的token类型,然后是关于token位置的定义。/ Position 表示的是源文件当中某个记号的位置type Position struct Filename string / filename, if any Offset int / offset, starting at 0 Line int / line number, starting at 1 Column int / column number, starting at 1 (byte count) 这个很简单就是标示在文件中的位置,Pos的定义 type Pos int 是位置标示的紧凑表示.接下来看看Pos和Position之间是如何转换的. 首先定义了一个 FileSet,可以理解为把 File 的内容字节按顺序存放的一个大数组,而某个文件则属于数组的一个区间base,base+size中,base是文件的第一个字节在大数组中的位置,size是这个文件的大小,某个文件中的 Pos 是在base,base+size这个区间里的一个下标。 最后 Pos 能够压缩成一个整数来表示一个文件当中的位置,当需要使用的使用再从 FileSet 中转换出完整的 Position 对象。 所以整个token包只是对token的一些定义和转化,词法分析的部分在scanner当中。 scan的主流程如下,主体是一个switch case
温馨提示
- 1. 本站所有资源如无特殊说明,都需要本地电脑安装OFFICE2007和PDF阅读器。图纸软件为CAD,CAXA,PROE,UG,SolidWorks等.压缩文件请下载最新的WinRAR软件解压。
- 2. 本站的文档不包含任何第三方提供的附件图纸等,如果需要附件,请联系上传者。文件的所有权益归上传用户所有。
- 3. 本站RAR压缩包中若带图纸,网页内容里面会有图纸预览,若没有图纸预览就没有图纸。
- 4. 未经权益所有人同意不得将文件中的内容挪作商业或盈利用途。
- 5. 人人文库网仅提供信息存储空间,仅对用户上传内容的表现方式做保护处理,对用户上传分享的文档内容本身不做任何修改或编辑,并不能对任何下载内容负责。
- 6. 下载文件中如有侵权或不适当内容,请与我们联系,我们立即纠正。
- 7. 本站不保证下载资源的准确性、安全性和完整性, 同时也不承担用户因使用这些下载资源对自己和他人造成任何形式的伤害或损失。
最新文档
- 藏品买卖合同样本
- 保安公司加盟合同样本
- 京东快递收件合同样本
- 兼职定金合同样本
- 加装电梯标准合同样本
- 买卖生蚝合同样本
- 介绍电梯居间合同样本
- 仓储实训合同样本
- 制员聘用合同样本
- 刊登广告合作合同样本
- 边缘计算与5G融合技术研究-全面剖析
- 浙江省台州市2025届高三第二次教学质量评估化学试题及答案(台州二模)
- 磁分离技术在天然气管道黑粉处理中应用的研究与效果分析
- 选煤厂安全管理制度汇编
- 住房公积金个人账户合并申请表(文书模板)
- 部编版八年级历史(下)全册教案
- 叉车驾驶员培训手册-共89页PPT课件
- 面试无机化学研究前沿ppt课件
- 【项目申报书】神经环路的形成、功能与可塑性
- 金属矿床地下开采——矿床开拓方法设计(完整版)
- 《工厂安全用电常识》
评论
0/150
提交评论