What is the difference between implementing a compiler and an interpreter?
我最近读了整本龙书(只是为了好玩,我真的不打算实现一个真正的编译器),而我的脑海中浮现着这个大问题。
实现编译器和解释器有什么不同?
对我来说,编译器由以下部分组成:
- 词法分析器
- 解析器(构建语法树)
- 生成中间代码(如3地址代码)
- 如果你愿意,可以做所有这些疯狂的事情:-)
- 从3地址代码生成"汇编"或"本机代码"。
现在,显然,解释器也具有与编译器相同的词法分析器和解析器。
但那之后呢?
-
它是否"读取"语法树并直接执行它? (有点像指针指向树中的当前节点,执行是一个大树遍历加上调用堆栈的内存管理)(如果是这样,它是如何做到的?我希望执行比检查它是什么类型的节点的巨大switch语句更好
-
它会生成3个地址代码并解释它吗? (如果是这样,它是如何做到的?再次,我正在寻找比一英里长的开关声明更优雅的东西)
- 它是否生成真正的本机代码,将其加载到内存中并使其运行? (此时我猜它不再是解释器了,但更像是JIT编译器)
此外,"虚拟机"的概念在哪一点上切入?你在一种语言中使用虚拟机是什么? (要清楚我的无知程度,对我来说虚拟机是VMWare,我不知道VM的概念如何应用于编程语言/执行程序)。
如你所见,我的问题非常广泛。我主要不仅要寻找使用哪种方法,而且主要是先了解大概念,然后详细了解它的工作原理。我想要丑陋的原始细节。显然,这更像是对要阅读的东西的追求,而不是期望你在这里回答所有这些细节。
谢谢!
丹尼尔
编辑:感谢您的答案到目前为止。我意识到我的头衔有误导性。我理解编译器和解释器之间的"功能"差异。
我正在寻找的是你如何实现解释器与编译器的区别。
我现在明白如何实现编译器,问题是解释器与此有何不同。
例如:VB6显然既是编译器又是解释器。我现在了解编译器部分。但是,我无法理解,当在IDE内部运行时,它可以让我在任意点停止程序,更改代码,并使用新代码继续执行。
这只是一个很小的例子,它不是我正在寻找的答案。正如我在下面解释的那样,我想要理解的是在我有一个解析树之后会发生什么。编译器将以"目标"语言从中生成新代码。口译员做什么?
谢谢您的帮助!
编译器是将一种编程语言的程序翻译成另一种编程语言的程序的程序。就是这样 - 简单明了。
解释器将编程语言翻译成其语义含义。
x86芯片是x86机器语言的解释器。
Javac是java到java虚拟机的编译器。 java,可执行应用程序,是jvm的解释器。
一些解释器共享一些编译元素,因为它们可能将一种语言翻译成另一种更易于解释的内部语言。
解释器通常(但并非总是)具有读取 - 评估 - 打印循环。
简答:
- 编译器将源代码转换为可执行格式以供稍后执行
- 解释器评估源代码以立即执行
如何实施这些方面还有很大的余地。解释器可以生成本机机器代码然后执行该代码,而虚拟机的编译器可以生成p代码而不是机器代码。像Forth这样的线程解释语言在字典中查找关键字并立即执行其关联的本机代码函数。
编译器通常可以更好地优化,因为他们有更多的时间来研究代码并生成一个文件供以后执行;口译员有更少的时间进行优化,因为他们倾向于在第一眼看到"按原样"执行代码
在后台优化的解释器,也可以学习更好的方法来执行代码
摘要:差异真正归结为"为以后的执行准备代码"或"立即执行代码"
程序是您想要完成的工作的描述。
编译器将高级描述转换为更简单的描述。
口译员会阅读有关做什么和做什么的描述。
- 一些解释器(例如Unix shell)一次读取一个小块的描述,并在它们看到时对每个块进行操作;一些(例如Perl,Python)读取整个描述,在内部将其转换为更简单的形式,然后对其进行操作。
- 一些解释器(例如Java的JVM或Pentium 4芯片)只能理解一种非常简单的描述语言,这种描述语言对于人类直接使用而言过于繁琐,因此人们使用编译器将其高级描述转换为该语言。
编译器从不做这项工作。口译员总是做这项工作。
两者都有很多共同点(例如词法解析器),并且在差异上存在分歧。我这样看:
经典定义是编译器将符号流解析并转换为可由CPU运行的字节流,而解释器执行相同的操作但将它们转换为必须在一个软件上执行的形式(例如, JVM,CLR)。
然而人们称'javac'为编译器,因此编译器的非正式定义必须是源代码作为单独的步骤,而解释器没有"构建"步骤(例如PHP,Perl)。
它不像过去那样清晰。它曾经是构建一个解析树,绑定它并执行它(通常在最后一秒绑定)。
BASIC主要以这种方式完成。
您可以声称在没有执行JIT的情况下运行字节码(java / .net)的内容是interpriters - 但不是传统意义上的,因为您仍然需要'编译'到字节码。
旧学校的区别在于:如果它生成CPU代码,那么它就是编译器。如果您直接在编辑环境中运行它并且可以在编辑时与它进行交互,那么它就是一个interpriter。
这远不如实际的龙书那么正式 - 但我希望它能提供丰富的信息。
关于你的问题的这一部分,其他答案尚未真正解决:
Also, at which point does the concept
of"virtual machine" cut in? What do
you use a virtual machine for in a
language?
像JVM或CLR这样的虚拟机是一个抽象层,允许您为编译为在VM上运行的完全不同的语言重用JIT编译器优化,垃圾收集和其他实现细节。
它们还可以帮助您使语言规范更加独立于实际硬件。例如,虽然C代码在理论上是可移植的,但如果您真的想要生成可移植代码,则必须经常担心字节顺序,类型大小和变量对齐等问题。对于Java,JVM在这些方面非常明确,因此语言设计者及其用户不必担心它们; JVM实现者的工作是在实际硬件上实现指定的行为。
如果我的经验表明什么;
在镜头中,差异可能很简单
1 2 3 | case '+': symtbl[var3] = symtbl[var1] + symtbl[var2]; break; |
之间,
1 2 3 | case '+': printf("%s = %s + %s;",symtbl[var3],symtbl[var1],symtbl[var2]); break; |
(如果您使用其他语言或(虚拟)机器指令,则无关紧要。)
一旦解析树可用,就有几种策略:
1)直接解释AST(Ruby,WebKit的原始解释器)
2)代码转换 - >转换为字节代码或机器代码
要实现编辑和继续,必须重新计算和移动程序计数器或指令指针。这需要IDE的合作,因为代码可能是在黄色小箭头之前或之后插入的。
可以这样做的一种方法是将程序计数器的位置嵌入到解析树中。例如,可能会有一个称为"break"的特殊语句。程序计数器只需在"break"指令后定位即可继续运行。
此外,您必须决定要对当前堆栈帧(以及堆栈上的变量)执行的操作。也许弹出当前堆栈,复制变量或保持堆栈,但在GOTO中修补并返回到当前代码。
如果您正在寻找一本书,计算机程序的结构和解释("向导书")是一个从解释器概念开始的好地方。你只需要处理Scheme代码,它可以被遍历,评估和传递,就像它是一个AST一样。
另外,Peter Norvig有一个简短的例子,解释了使用Python的主要思想(在评论中有更多的例子),这是维基百科上的另一个小例子。
就像你说的那样,它是一个树遍历,至少对于按值调用它是一个简单的:每当你看到一个运算符时,先评估操作数,然后应用运算符。返回的最终值是程序(或给REPL的语句)的结果。
请注意,您并不总是必须明确地执行树遍历:您可以以接受访问者的方式生成AST(我认为SableCC会这样做),或者用于非常小的语言,例如用于演示的小型算术语法解析器生成器,您只需在解析期间评估结果。
为了支持声明和分配,您需要保持环境。正如您通过添加操作数来评估"加号"一样,您可以通过在环境中查找来评估函数,变量等的名称。支持范围意味着将环境视为堆栈,并在适当的时间推送和弹出。通常,解释器的复杂程度取决于您支持的语言功能。例如,口译员可以进行垃圾收集和内省。
对于VM:plinth和j_random_hacker将计算机硬件描述为一种解释器。反之亦然 - 口译员是机器;他们的指令恰好比真正的ISA更高级。对于VM风格的解释器,程序实际上类似于机器代码,albiet用于非常简单的机器。 Java字节码只使用几个"寄存器",其中一个寄存器包含一个程序计数器。因此,VM解释器更像是硬件模拟器而不是上面链接的示例中的解释器。
但请注意,出于速度原因,默认的Oracle JVM通过将Java字节码指令的运行转换为x86指令("及时编译")来工作。
根据您的步骤列表:
- 词法分析器
- 解析器(构建语法树)
- 生成中间代码(如3地址代码)
- 如果你愿意,可以做所有这些疯狂的事情:-)
- 从3地址代码生成"汇编"或"本机代码"。
一个非常简单的解释器(如早期的BASIC或TCL)只能一次执行第一步和第二步。然后在继续执行下一行的同时丢弃大部分结果。其他3个步骤根本不会执行。