闲谈联系编译原理学语言:探索编译器的世界

        编写一个编译器是一项复杂且极具挑战性的系统工程,它涵盖了多个关键领域,全面体现了设计、编码、算法以及数据结构的综合运用。从本质上讲,编译器的设计过程分阶段进行,这种模块化的构建方式充分展示了系统设计的精妙之处。编译器理论的发展深受诸多理论的影响,其中乔姆斯基这位专注于自然语言研究、原本与计算机领域并无直接关联的学者的研究成果,为编译器理论的发展奠定了重要基础。在此之后,一整套完善的编译理论逐步形成,包括图灵机理论、正规词法逻辑等高层逻辑模型,它们共同构成了现代编译器的理论基石。

        在实际编码环节,实现一个具体的编译器自然离不开编写代码,这是将理论转化为实际可运行程序的关键步骤。算法在编译器中也起着举足轻重的作用,例如,语法的BNF(巴克斯-诺尔范式)设计就是一个庞大而复杂的算法过程,它涉及到对语法规则的精确描述和定义。此外,验证图灵机是否会终止的算法也是编译器中不可或缺的一部分,它关乎编译器在处理各种输入时的行为和结果。在数据结构方面,树与递归频繁出现在编译器的设计中。这里的数据结构更多地是基于离散数学意义上的离散结构,如树、环、图等,此时它们尚未涉及到具体使用何种编程语言进行编码的层面,而是处于通用的理论层次,为后续的具体实现提供了理论框架。

        以C语言的视角来看,开发一套编译器的全过程,是算法与数据结构完美结合的集中体现。在这一过程中,C语言凭借其对底层的强大操控能力,能够精确地实现编译器所需的各种复杂逻辑。虽然Java也具备开发编译器的能力,但其拥有诸如OO(面向对象)等高层设计工具,这使得Java在算法和数据结构的体现上相对C语言没有那么深入。而且,Java在编译器后端的工作中往往显得力不从心,因此在实际应用中,Java一般较少用于开发编译器。

        在编译器这个系统工程的前期阶段,编译器前端(包括词法分析、语法分析以及中间代码生成)的大部分理论主要基于编译理论的高级逻辑。由于存在统一的理论以及相关工具,如yacc和lex等,其实现方法和路径相对较为单一。词法分析作为编译器前端的重要环节,其任务是从一段源程序中读取内容,并将其解析为一个一个的lex单元,以供后续的语法分析使用。在这个过程中,字符串成为编译器唯一直接处理的对象,因为当前我们编写程序的方式是面向行的,每一行语句都对应着中间代码的一个三元式,并且在编译时会为每一行给定行标号,以产生相应的内存地址,这就决定了编译器是面向文本的。

        正规式和自动机理论为词法分析带来了科学、合理的方法,就如同大家普遍使用BNF来描述文法一样,它们统一了词法分析的过程。形象地说,有人认为NFA(非确定有限自动机)是给机器理解的,而正规式是给人阅读和编写的。但实际上,编写词法分析逻辑时,也可以不依赖正规式与自动机这样相对迂回的逻辑模型。对于那些代码控制能力强的开发者来说,他们可以直接编写此类逻辑。

        当进入编译器后端时(包括代码生成、代码优化、运行时环境构建、错误处理和调试等环节),就需要深入到平台逻辑层面。这一阶段是编译器开发中产生分歧的关键所在,也是充满难点且可以无限深入研究的部分。一般而言,编译原理通常主要指编译器前端,因为后端的工作不再单纯属于编译知识范畴,而是更多地涉及平台处理逻辑。当完成中间代码生成时,实际上已经实现了从高级源程序到代码的初步转换,尽管此时生成的是中间代码,后续还需要经过汇编等步骤才能得到最终的目标语言代码。但需要注意的是,即便不进行后续步骤,我们也可以通过发展一个虚拟机内置解释器来执行这些中间代码。由于后端的具体操作因目标平台的不同而存在差异,缺乏统一的理论,例如代码优化就是一个没有固定定论的过程,不同的平台和应用场景可能需要采用不同的优化策略。

        理解编程语言的类型也是学习编译原理过程中的重要一环。例如,动态语言是指在运行时其运行环境是动态可变的,新的函数可以在运行过程中被引入。动态类型语言则是指类型可以在运行期被改变的语言,通常来说,类型系统是一门语言的重要特征之一,如果类型在运行期能够动态变化,那么该语言就是运行期动态的。弱类型语言虽然有类型的概念,但类型之间的转换并非严格受限,例如字符串可以经过转型后用作数值型。无类型语言则不需要在编写代码时显式地声明类型,其类型在运行时根据赋予的值来确定,并且还可以再次变动,这就是所谓的ducking type(鸭子类型),即“如果它看起来像鸭子,走起来像鸭子,那么它就是一只鸭子”。在这种语言中,由于没有类型的明确概念,也就不存在传统意义上的变量(因为变量通常是类型的代表),一切由值决定,只存在值,而没有变量。

        编译器后端会针对目标平台生成相应的代码,这里的目标平台包括硬件机器和操作系统(OS),其中操作系统起着更为关键的作用。因为操作系统封装了系统调用(例如BIOS中原子基本的几条IO功能接口,可供操作系统和上层系统进行后续的抽象)。从编程语言的角度来看,封装后的操作系统与语言的其他库类似,都属于一种语言逻辑,只要有接口就可以进行调用。语言与操作系统并非简单的因果循环关系,通常是先有操作系统,然后才会出现特定运行于该操作系统下的语言实现,即编译器。甚至有些小型编译器可以实现自编译。

        那么,在操作系统环境下,如何运行由特定语言编写的代码呢?这就涉及到代码如何从操作系统中获取运行所需的空间等资源,以及如何通过system call进行IO操作来处理文件(例如C语言标准库stdio中的文件操作实际上就是封装了system call io逻辑)。可以将运行时看作是语言后端逻辑(编译后端代码生成器及其生成的代码)与操作系统逻辑之间的接口。对于操作系统来说,它要解决的是如何运行该语言代码的问题;对于代码而言,它需要解决的是如何从操作系统中获取资源来支持自身运行,并提供像main()这样的程序入口。当然,特定的编译器厂商在开发完整的编译器时,甚至会包含一些语言实现的标准库动态链接库(std lib dll)。不过,一般将std lib dll简单等同于C运行时库(crt)是不准确且不完整的。实际上,只有C语言有标准编译器实现必须实现的crt,从这个意义上讲,运行时是语言的库逻辑,同时也是编译后端的重要组成部分。对于每一种语言,包括那些没有标准库的语言,都需要运行时,从这个层面理解,它是系统软件,是语言与系统之间的接口,是一套完整编译器实现所必不可少的。从解释器的角度来看,解释器并不为特定目标生成代码,它主要负责运行语言的源代码(尽管这其中可能会生成中间代码,但解释器并不运行目标代码,其目标就是解释器自身,目标代码就是中间代码)。在这种情况下,解释器类似于机器和操作系统,它没有C运行时库,而在编译环境下的crt相当于额外的中间层。以.net的通用运行时为例,其原理也是如此。

        运行时环境的概念也需要深入理解。XML最初是为解决数据异构问题而出现的,如果不了解这一历史背景,就容易将XML仅仅视为一种新的理论和实践,而忽略其产生的根本原因和存在的本质。运行时实际上也可以称为运行空间,它是程序运行的物质环境,包括CPU架构(其中的寄存器等部件是专门为程序运行而设置的,这里的程序并非仅指某一门语言的程序)。当然,运行时也可以是虚拟机等软件逻辑层面的内容。实际上,程序不可能单纯以硬件作为运行环境(除非是电器化的微指令或裸体指令,但此时还没有程序的完整概念),必须有操作系统逻辑来封装CPU硬件逻辑,然后供运行时使用。因为单纯提出一个CPU,它只有指令,并没有程序的概念,只有当操作系统机制、高级语言的汇编原理出现,以及程序机制发展之后,才产生了运行时这个说法。

        编译与解释是编程语言实现中的两种重要方式。CPU和操作系统所构成的本地环境为我们提供了开发的基础空间。从某种意义上讲,云计算并不是一个全新的概念,它实际上等同于web 2.0、web x.x与网络计算机(NC)的结合。在未来的云计算模式下,我们日常使用的计算机可能会逐渐演变成NC,无需为没有CPU和操作系统的“本地”进行编程和开发软件,所有软件都将在“远程”的WEB以及其他服务器上进行开发。也就是说,以后只有云计算机才会拥有OS和CPU,而云计算机将不必为本地开发软件,除非本地计算机成为一个NC空壳。否则,只要我们现在使用的计算机拥有强大的CPU和操作系统,对它的开发工作就会一直存在。在云计算环境下,以后所有的云计算机(即服务器)才有OS和CPU,而我们人手一台的PC将严格不能成为服务器。例如,由于我们的工作站没有NT操作系统,所以只能作为上网用的浏览器这样的简单终端,而不能成为提供服务的云计算机。NC概念早在多年前就已被提出,而web2.0是相对较新的概念,因此云计算可以说是两者不新不老的结合体。

        CPU和操作系统的环境为我们提供了特定的“本地”编程空间,同时也带来了相应的限制。而WEB“远程”计算则提供了不同的编程空间和编程限制。要理解这两者的差异,首先需要理解它们的抽象基础。CPU提供了冯·诺依曼架构,因此需要有输入输出(IO)和寄存器。操作系统则提供了进程管理、内存管理、应用程序编程接口(API),进而提供了解释器。编译是直接面向硬件的,但实际上并不存在绝对的编译语言或解释语言。编译的优势在于可以为特定平台进行优化,而解释则无法做到这一点。

        运行期与编译期也是学习编程语言和编译原理时需要关注的重要概念。就像有些知识的产生是为了解决特定领域与人类交互的问题(例如OO不仅是技术问题,更是软件工程问题,它的出现是为了解决编程方式与人类思维的结合问题),在探索编程相关领域时,往往不可避免地会涉及到其他领域的知识。我们当前的编程方式,包括如何编写流程、处理异常等,不仅仅是单纯的编程知识,还与程序的运行环境密切相关。因此,必须先掌握编译原理抽象领域的知识,才能熟练运用一门编译语言进行编程工作。

        编程范式,即编程习惯,如函数编程法、OO编程法、AOP编程法等,是由计算机处理数据的方法和内部逻辑所决定的。例如,函数语言就是由lambda演算发展而来的,函数语言具有一种其他范式语言所没有的eval()过程。计算的原理本质上是一种图灵机的离散形式,因此是命令式的,只需一个入口和一堆待处理的数据,就能串行得到另一个结果(这就是串行形式算法,计算机的功能本质上就是状态的改变,随着未来并行计算和多核技术的发展,编程也将出现并行范式)。这个结果可以作为中间结果被另一个串行处理过程所使用。计算机(堆栈机)或虚拟机(如JVM这样的软件机器)是构建在这一基础之上的另一层抽象。在这种开发方式下,需要关注许多算法,包括离散算法和数学算法。以递归和迭代为例,迭代是计算机处理数据的一种常见方式,它更侧重于行动导向;而递归则更偏向于目标导向,更符合人类的思维方式。例如,在函数语言中,使用迭代可能更为合适;而在OOP语言中,递归则更为常用,因为计算机在处理离散形式的数据时,迭代方式更为高效,迭代通常需要一个循环变量,而递归则不需要。

        编译期和运行期分开具有深刻的本质和抽象意义。实际上,字符串逻辑和数据结构逻辑在一定程度上可以与内存无关,编程语言可以站在一个较高的抽象角度来处理这些逻辑。例如,一门语言甚至可以将字符串抽象为rope(当然也可以是其他形式),因为语法级别的设计抽象可以与运行期没有直接关联。编译期后端才负责程序运行相关的事务,此时才会涉及到运行效益的问题。因此,语法级,也就是设计期,可以站在一个完全脱离运行逻辑的抽象高度上对字符串等进行抽象。语法级主要由C语言、C++、JAVA等高级语言负责,它们只关注这方面的高级逻辑。编译前端和后端是可以分开的,只有到编译后端时,才需要将高级逻辑映射到汇编语言这样的机器逻辑。

        C语言通过底层方式来表达语法级的设计抽象,例如它将字符串看作是指针数组,这使得C语言在运行期产生的抽象与语言级抽象最为接近。虽然是运行抽象,但几乎等同于设计,因此C语言适用于内存有限的特殊平台,如手机等。尽管如此,C语言的抽象能力依然强大,它可以实现对OOP、结构等的抽象。相比之下,C++在语法级直接支持更大程度的抽象,它继承了C语言的核心(如流程控制等,C语言被称为最小内核语言),并在较大抽象层面上沿袭了C语言的指针和预编译这两大抽象模块,同时自身发展出运行期的OO特性。面向运行期的模板使得C++能够产生STL这样的泛型抽象集,面向受限编译期的模板则产生了BOOST这样的元编程抽象集。而其他第四代语言,如JAVA、RUBY、LUA、PYTHON等,在语法级直接支持的设计抽象更为丰富,因为它们不像C语言那样需要处处考虑运行期的限制,而是更侧重于满足人类的设计需求,至于运行期的性能问题,则主要依赖于虚拟机的性能。 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值