编程语言的哪些属性使得无法进行编译?


71

题:

“某种编程语言的某些属性可能要求将代码写入其中的唯一方法是通过解释来执行。换句话说,不可能编译成传统CPU的本机代码。这些属性是什么?”

编译器:Parag H. Dave和Himanshu B. Dave的原则和实践(2012年5月2日)

这本书没有给出答案的任何线索。我试图找到有关编程语言概念(SEBESTA)的答案,但无济于事。网络搜索也无济于事。你有什么线索吗?


31
著名的是,Perl甚至无法解析。除此之外,在没有进一步假设的情况下,这种说法似乎是错误的:如果有解释器,我总是可以将解释器和代码捆绑在一个可执行文件中,瞧。
拉斐尔

4
@Raphael:好主意,但是... 1)您假设代码在执行之前可用。这不适用于交互式用途。当然,您可以对bash语句或PostScript堆栈内容的本地代码使用即时编译,但这是一个非常疯狂的主意。2)您的想法实际上并没有编译代码:捆绑包不是代码的编译版本,而是代码的解释器。
reinierpost 2014年

7
在过去的好日子里,我有自编辑gwbasic程序(gwbasic用一种字节码存储基本程序)。我目前无法想到一种明智的方式来将其编译为本地机器代码,同时保留其自身的编辑能力。
PlasmaHH 2014年

15
@PlasmaHH:自修改代码可以追溯到1948年。第一个编译器写于1952年。自修改代码的概念是在本机机器代码中发明的
Mooing Duck

10
@reinierpost Raphael就此问题采取了理论立场。它具有显示问题的概念局限性的优点。编译是从语言S到语言T的翻译。语言T可能是S的扩展,可以在其中添加其他语言的解释代码。因此,将S及其解释器捆绑在一起是使用T语言编写的程序。对于工程师来说,这似乎很荒谬,但是它表明,要有意义地提出问题并不容易。从工程角度来看,您如何区分可接受的编译过程和不可接受的编译过程(例如Raphael的过程)?
2014年

Answers:


61

拉斐尔(Raphael)的评论强调了解释型代码与编译型代码之间的区别可能是虚构

the claim seems to be trivially wrong without further assumptions: if there is
an interpreter, I can always bundle interpreter and code in one executable ...

事实是,代码始终是通过软件,硬件或两者的组合来解释的,而编译过程无法确定它将是哪种。

您认为编译是从一种语言(对于源语言)到另一种语言(对于目标语言)的翻译过程。并且,的解释器通常与的解释器不同。STST

编译程序从一个语法形式翻译到另一个语法形式,使得在给定的语言的预期语义和,和具有相同的计算性能,高达几件事,你通常试图可能会进行优化的更改,例如复杂性或简单的效率(时间,空间,表面,能耗)。我试图不谈论功能对等,因为这需要精确的定义。PSPTSTPSPT

实际上,某些编译器仅用于减少代码的大小,而不是“改善”执行力。柏拉图系统中使用的语言就是这种情况(尽管他们没有将其称为编译)。

如果在编译过程之后不再需要的解释器,则可以认为代码已完全编译。至少,这是我作为工程而非理论问题来阅读您的问题的唯一方法(因为从理论上讲,我始终可以重建解释器)。S

afaik可能引起问题的一件事是元循环。那时程序将以其自己的源语言操纵语法结构,从而创建程序片段,然后将其解释为原始程序的一部分。由于您可以通过操纵无意义的句法片段的任意计算而以语言生成任意程序片段,因此我想您几乎不可能(从工程学角度)将程序编译为语言,从而现在它会生成片段。因此,将需要用于的解释器,或者至少需要从到SSTTSST用于即时编译中生成的片段(另请参见本文档)。S

但是我不确定如何将其正确形式化(并且现在没有时间)。而不可能是未正式问题一个大词。

进一步的评论

36小时后添加。您可能要跳过很长的续集。

关于这个问题的许多评论都显示了对该问题的两种观点:一种理论上的观点认为它毫无意义,而一种工程学上的观点不幸地不那么容易形式化。

查看解释和编译的方法有很多,我将尝试画一些方法。我会尽力做到非正式

墓碑图

T或墓碑图是早期形式化(1960年代初期至1990年代末)之一 。这些图以可组合的图形元素表示,其中包括解释程序或编译器的实现语言,要解释或编译的源语言以及就编译器而言的目标语言。更详尽的版本可以添加属性。这些图形表示可以看作是公理,推论规则,可用于从公理àla Curry-Howard的存在证明中机械地推导处理器的生成(尽管我不确定这是在60年代完成的)。

部分评估

另一个有趣的观点是部分评估范式。我只是简单地将程序视为一种函数实现,可以根据给定的一些输入数据来计算答案。然后,语言的解释器 是一个程序,该程序 用编写的程序和该程序的数据,并根据的语义计算结果。当仅知道一个自变量,部分求值是一种专门用于两个自变量和的程序的技术。目的是在您最终获得第二个参数时进行更快的评估ISSpSSdSa1a2a1a2。如果更改频率比更改频率更高,则特别有用,因为可以在仅更改所有计算中分摊使用进行部分评估的成本。a2a1a1a2

这是算法设计中的一种常见情况(通常是SE-CS上的第一条评论的主题),当对数据中一些更静态的部分进行预处理时,可以在所有应用程序上分摊预处理成本具有更多可变部分输入数据的算法。

这也是解释器的情况,因为第一个参数是要执行的程序,通常使用不同的数据执行多次(或者子部分使用不同的数据执行多次)。因此,对解释器进行专业化处理成为一个自然的想法,方法是通过首先在该程序上对它进行部分评估来更快地评估给定程序。这可能被视为编译程序的一种方式,并且在通过对解释器的第一个(程序)参数进行部分评估来进行编译方面,已经进行了大量的研究工作。

Smn定理

关于部分评估方法的一个好处是它确实扎根于理论(尽管理论可能是骗子),尤其是在 克莱因的Smn定理中。我在这里试图对其进行直观的介绍,希望它不会使纯粹的理论家感到不安。

给定递归函数的Gödel编号,您可以将视为您的硬件,因此,给定程序的Gödel编号 (读取目标代码),是定义的函数(即由目标代码在您的硬件)。φφpφpp

以最简单的形式,该定理在Wikipedia中陈述如下(在符号上稍有变化):

给定递归函数的Gödel编号,存在具有以下属性的两个参数的原始递归函数:对于具有两个参数的部分可计算函数每个Gödel数,表达式和为自然数和的相同组合定义,并且对于任何这样的组合,它们的值都相等。换句话说,对于每个,以下函数的扩展等式成立: φσqfφσ(q,x)(y)f(x,y)xyxφσ(q,x)λy.φq(x,y).

现在,以作为解释器,作为程序的源代码,作为该程序的数据,我们可以编写: qISxpSydφσ(IS,pS)λd.φIS(pS,d).

φIS可以看作是解释器 在硬件上的执行,即可以看作是黑盒,可以解释用语言编写的程序。ISS

功能可以视为专门解释的功能为程序,如在部分评价。因此可以看到哥德尔数目标代码是程序的编译版本。σISPSσ(IS,pS)pS

因此函数可以看作是一个 以语言编写的程序的源代码作为参数并返回该程序的目标代码版本的函数因此,通常称为编译器。CS=λqS.σ((IS,qS)qSSCS

一些结论

但是,正如我所说:“理论可以是骗子”,或者实际上似乎是一个骗子。问题在于我们对函数一无所知。实际上有很多这样的函数,我的猜测是定理的证明可能使用一个非常简单的定义,从工程的角度来看,这可能比Raphael提出的解决方案更好:将这些捆绑在一起源代码与解释器。这总是可以做到的,因此我们可以说:编译始终是可能的。q 小号小号σqSIS

正式定义什么是编译器的更严格的概念将需要更微妙的理论方法。我不知道在这个方向上可能做了什么。从工程的角度来看,在部分评估上完成的非常实际的工作更加现实。当然,还有其他一些用于编写编译器的技术,包括从基于Curry-Howard同构的类型理论的上下文中开发的规范证明中提取程序(但是我已经超出了我的能力范围) 。

我在这里的目的是表明拉斐尔的言论并非“疯狂”,而是理智地提醒人们事情并不明显,甚至不简单。说些不可能的事情是一个强有力的陈述,即使只是要对如何和为什么不可能做到有准确的了解,也确实需要精确的定义和证明。但是,建立适当的形式化形式来表达这种证明可能非常困难。

这就是说,即使是无法编译的特定功能,如工程师所理解的那样,正如Gilles的回答所表明的那样,标准编译技术始终可以应用于不使用该功能的程序部分。

要遵循Gilles的主要观点,即取决于语言,某些事情可能在编译时完成,而其他事情必须在运行时完成,因此需要特定的代码,我们可以看到编译的概念实际上是定义不明确,可能无法以任何令人满意的方式定义。正如我在部分评估部分中试图展示的那样,当我将编译器与某些算法中的静态数据预处理进行比较时,编译只是一个优化过程。

作为一个复杂的优化过程,编译的概念实际上属于一个连续体。根据语言或程序的特性,某些信息可能是静态可用的,并允许进行更好的优化。其他事情必须推迟到运行时。当情况变得非常糟糕时,至少在程序的某些部分必须在运行时完成所有工作,并且将源代码与解释器捆绑在一起是您所能做的。因此,这种捆绑只是该编译连续体的低端。关于编译器的许多研究都是关于寻找静态地完成以前动态完成的方法的。编译时垃圾回收似乎是一个很好的例子。

请注意,说编译过程应该产生机器代码并没有帮助。这正是捆绑程序可以做的,因为解释器是机器代码(嗯,交叉编译会使事情变得更复杂)。


3
不可能是一个大词”一个非常非常大的词。=)
Brian S

3
如果将“编译”定义为是指一系列步骤,这些步骤完全执行程序接收其第一输入之前发生,并且解释为通过数据控制程序流(不是程序抽象机器模型的一部分)进行的过程,然后对于要编译的语言,编译器必须有可能在执行开始之前识别语言构造可能具有的所有可能含义。在语言构造可能具有无限含义的语言中,编译将不起作用。
supercat

@BrianS不,不是,这不可能证明;)
Michael Gazonda 2014年

@supercat仍然不是一个定义。语言构造的“含义”是什么?
Rhymoid 2014年

我喜欢将编译器/解释器视为部分执行的概念!
Bergi 2014年

17

问题实际上不是关于编译是不可能的。如果可以解释一种语言¹,则可以通过将解释器与源代码捆绑在一起,以简单的方式进行编译。问题是,究竟哪种语言功能使这成为唯一途径。

解释器是一个程序,它将源代码作为输入并按照该源代码的语义指定的方式运行。如果需要解释器,则意味着该语言包括一种解释源代码的方法。此功能称为eval。如果在语言的运行时环境中需要解释器,则表示该语言包括eval:或者eval作为原始语言存在,或者可以某种方式对其进行编码。eval与大多数Lisp方言一样,称为脚本语言的语言通常也包含一项功能。

仅仅因为一种语言包括在内eval并不意味着它的大部分不能被编译成本地代码。例如,有一些优化的Lisp编译器,它们可以生成良好的本机代码,并且仍然支持evaleval编辑的代码可能会被解释,也可能会即时编译。

eval是最终的“需要解释器”功能,但是还有其他一些功能需要缺少解释器。考虑编译器的一些典型阶段:

  1. 解析中
  2. 类型检查
  3. 代码生成
  4. 连结中

eval意味着所有这些阶段都必须在运行时执行。还有其他功能使本机编译困难。从底层开始,某些语言通过提供函数(方法,过程等)和变量(对象,引用等)可以依赖于非本地代码更改的方式来鼓励后期链接。这使得很难(但并非不可能)生成高效的本机代码:将对象引用作为调用保留在虚拟机中更容易,并且让VM引擎动态处理绑定。

一般来说,反射倾向于使语言难以编译为本地代码。评估原语是反射的极端情况。许多语言都没有走那么远,但是仍然具有根据虚拟机定义的语义,例如,允许代码按名称检索类,检查其继承,列出其方法,调用方法等。使用JVM的Java和带有.NET的C#是两个著名的示例。实现这些语言的最直接的方法是将它们编译为字节码,但是仍然有本机编译器(许多即时),它们至少编译了不使用高级反射功能的程序片段。

类型检查确定程序是否有效。不同的语言对于在编译时和运行时执行多少分析有不同的标准:如果一种语言在开始运行代码之前执行了很多检查,则称为“静态类型”;如果没有,则称为“动态类型”。某些语言包括动态转换功能或编组和类型检查功能;这些功能需要在运行时环境中嵌入类型检查器。这与在运行时环境中包括代码生成器或解释器的要求正交。

¹ 练习:定义一种无法解释的语言。


(1)我不同意将解释器与源代码捆绑为编译器,但您的其余文章非常出色。(2)完全同意评估。(3)我不明白为什么反射会导致语言难以编译为本地代码。Objective-C具有反射功能,并且(我认为)它通常是编译的。(4)模糊相关的说明,C ++模板超魔术通常是解释而不是编译然后执行。
Mooing Duck

刚出现在我身上,Lua已编译。在eval简单地编译字节码,然后作为一个单独的步骤中执行的二进制的字节码。它肯定在已编译的二进制文件中有反映。
Mooing Duck

在哈佛建筑机器上,编译应产生不需要被“数据”访问的代码。我认为源文件中的信息最终必须存储为数据而不是代码,因此并不是真正的“编译”。编译器接受像这样的声明int arr[] = {1,2,5};并生成包含[1,2,5]的Initialized-data节是没有错的,但是我不会描述其将[1,2,5]转换为机器代码的行为。如果几乎所有程序都必须作为数据存储,那么该程序的哪一部分真正被“编译”了?
supercat 2014年

2
@supercat这就是数学家和计算机科学家所说的琐碎的事。它符合数学定义,但是没有有趣的事情发生。
吉尔斯2014年

@Gilles:如果保留术语“编译”以翻译成机器指令(不保留关联的“数据”)并接受以编译语言表示的内容,则数组声明的行为不是“编译”数组,那么在某些语言中,不可能编译任何有意义的代码。
supercat 2014年

13

我认为作者认为编译意味着

  • 源程序不需要在运行时出现,并且
  • 运行时不需要编译器或解释器。

以下是一些示例功能,这些功能如果不是这种方案的“不可能”,将使其成为问题:

  1. 如果您可以在运行时查询变量的值,方法是通过变量名称(字符串)引用变量的值,那么您将需要在运行时使用变量名。

  2. 如果您可以在运行时调用函数/过程,只需通过其名称(为字符串)来引用它,那么您将在运行时需要该函数/过程名称。

  3. 如果您可以在运行时(作为字符串)构造一个程序,例如通过运行另一个程序,或者通过从网络连接中读取它等,那么您将需要在运行时使用解释器或编译器来构建程序。运行此程序。

Lisp具有所有三个功能。因此,Lisp系统始终在运行时加载解释器。语言Java和C#在运行时具有可用的函数名称,并使用表来查找它们的含义。诸如Basic和Python之类的语言可能在运行时也具有变量名。(我对此不是100%的肯定)。


如果将“解释器”编译到代码中怎么办?例如,使用分派表调用虚拟方法,这些是解释或编译的示例吗?
Erwin Bolwidt 2014年

2
“在运行时不需要编译器或解释器”,是吗?好吧,如果这是真的,那么从深度上讲,在大多数平台上也无法“编译”C。C运行时没有太多要做:启动,设置堆栈等,以及关闭进行atexit处理。但是它仍然必须存在。
别名2014年

1
“ Lisp系统始终在运行时加载解释器。” - 不必要。许多Lisp系统在运行时都有一个编译器。有些甚至根本没有口译员。
约尔格W¯¯米塔格

2
不错的尝试,但是en.wikipedia.org/wiki/Lisp_machine#Technical_overview。它们确实编译Lisp,并且旨在有效地执行结果。
彼得·施耐德

@Pseudonym:C运行时是一个库,而不是编译器或解释器。
Mooing Duck

8

当前的回复可能会“过分考虑”陈述/答案。可能是什么,作者指的是以下现象。许多语言都有类似“ eval”的命令;例如,参见javascript eval,它的行为通常作为CS理论的一个特殊部分进行研究(例如Lisp)。该命令的功能是在语言定义的上下文中评估字符串。因此,它实际上与“内置编译器”相似。直到运行时,编译器才知道字符串的内容。因此,在编译时无法将评估结果编译为机器代码。

其他的答案指出的解释VS编译型语言的区别可以显著在许多情况下,ESP更现代的语言,如说Java的一个模糊“即时编译”又名“热点”(JavaScript引擎如V8越来越多地使用同样的技术)。“类似评估”功能无疑是其中之一。


2
V8是一个很好的例子。它是一个纯编译器,永远不会进行任何解释。但是它仍然支持ECMAScript的完整语义,包括unrestricted eval
约尔格W¯¯米塔格

1
Lua也做同样的事情。
Mooing Duck

3

LISP是一个可怕的例子,因为它被认为是一种高级的“机器”语言,是“真实”语言的基础。所说的“真实”语言从未实现。LISP机器基于在硬件中执行(大部分)LISP的想法而构建。由于LISP解释器只是一个程序,因此原则上可以在电路中实现它。也许不切实际;但绝非不可能。

此外,有许多用硅编写的解释器,通常称为“ CPU”。解释(尚不存在,尚不存在……)机器代码通常很有用。例如,Linux的x86_64最初是在仿真器上编写和测试的。芯片上市时,手头已经有完整的发行版,甚至仅适用于早期采用者/测试者。Java通常被编译为JVM代码,这是一个解释器,用硅语言编写不会太难。

大多数“解释”语言都被编译成内部形式,然后对其进行优化和解释。例如,这就是Perl和Python所做的。还有一些用于解释性语言的编译器,例如Unix shell。另一方面,可以解释传统编译的语言。我看到的一个极端例子是使用解释器 C作为扩展语言的编辑器。C语言可以正常运行,但程序简单,没有问题。

另一方面,现代CPU甚至接受“机器语言”输入并将其转换为较低级的指令,然后在移交给执行之前对其进行重新排序和优化(即“编译”)。

整个“编译器”与“解释器”的区别实际上是没有意义的,在堆栈中的某个地方有一个最终的解释器,它接受“代码”并“直接”执行。程序员的输入沿线进行转换,其中的转换称为“编译”,只是在沙子上画了一条任意线。


1

实际情况是,解释某些Basic程序与执行汇编程序之间存在很大差异。在有或没有(及时的)编译器的情况下,P代码/字节代码介于两者之间。因此,我将尝试在这种现实的背景下总结一些要点。

  • 如果解析源代码的方式取决于运行时条件,则编写编译器可能变得不可能,或者变得如此困难,以至于没有人会打扰。

  • 修改自身的代码通常无法编译。

  • 使用类似eval的函数的程序通常无法事先完全编译(如果您将馈给它的字符串视为程序的一部分),尽管如果要重复运行eval的代码,则可能仍会使类似eval的函数调用编译器很有用。某些语言为编译器提供了一个API,可简化此操作。

  • 通过名称引用事物的能力并不排除编译,但是您确实需要表(如上所述)。以名称调用函数(例如IDispatch)需要大量的工作,以至于我认为大多数人都同意我们正在有效地讨论函数调用解释器。

  • 弱类型输入(无论定义如何)都会使编译更加困难,结果的效率可能降低,但通常并非不可能,除非不同的值触发了不同的解析。这里有一个滑动的比例尺:如果编译器无法推断出实际的类型,它将需要发出分支,函数调用等,否则将不会存在,从而将解释器的位有效地嵌入到可执行文件中。


1

我认为编程语言的主要功能是自我修改功能,该功能使该语言的编译器变得不可能(严格意义上,请参见self-hosting)。这意味着该语言允许在运行时更改源代码(某些情况下,编译器无法生成固定和静态的目标代码)。一个典型的例子是Lisp(另请参见同音)。使用许多语言(例如javaScript)中包含的语言构造(例如eval)提供了类似的功能。Eval实际上在运行时调用解释器(作为函数)

换句话说,该语言可以代表自己的元系统(另请参见元编程)。

请注意,在查询特定源代码的元数据以及可能仅修改元数据的意义上,语言反射(例如Java或PHP的反射机制)对于编译器而言没有问题,因为它已经具有元数据在编译时使用,并且可以根据需要将其提供给已编译程序。

使编译困难或不是最佳选择(但不是不可能)的另一个功能是语言中使用的键入方案(即,动态键入与静态键入以及强类型与松散类型)。这使编译器很难在编译时拥有所有语义,因此有效地使编译器的一部分(换句话说,解释器)成为生成的代码的一部分,该代码在运行时处理语义。换句话说,这不是编译而是解释


LISP是一个可怕的例子,因为它被认为是sprt
vonbrand

@vonbrand,也许但同时显示了同音概念和统一的数据代码对偶性
Nikos M.

-1

我觉得原始问题的格式不正确。该问题的作者可能打算提出一个稍微不同的问题:编程语言的哪些属性有助于为其编写编译器?

例如,为上下文无关的语言编写编译器要比上下文相关的语言容易。定义语言的语法也可能会遇到一些问题,例如歧义,使编译变得困难。此类问题可以解决,但需要额外的精力。类似地,与上下文相关的语言相比,由不受限制的语法定义的语言更难解析(请参阅Chomsky Hierarchy)。据我所知,使用最广泛的过程编程语言几乎与上下文无关,但是具有一些上下文相关的元素,使其相对易于编译。


2
这个问题显然是要反对/比较编译器和解释器。尽管它们的工作方式可能有所不同,并且除了上面的@Raphael极限情况外,它们通常会起作用,但是它们在语法分析和歧义性方面存在完全相同的问题。因此语法不是问题。我还相信,尽管过去已经存在语法问题,但如今它并不是当今编译器编写中的主要问题。我不是反对者:我喜欢发表评论。
2014年

-1

这个问题的答案如此明显,以至于通常被认为是微不足道的。但这在许多情况下确实很重要,并且是解释型语言存在的主要原因:

如果您还没有源代码,则不可能将源代码编译为机器代码。

解释器增加了灵活性,特别是它们增加了运行基础项目编译时不可用的代码的灵活性。


2
“我丢失了源代码”不是编程语言的属性,而是特定程序的属性,因此无法回答问题。而且您肯定需要引用这样的说法:避免丢失源代码是“解释性语言存在主要原因”,甚至它们存在原因。
David Richerby 2014年

1
@DavidRicherby我想用例tyleri想到的是交互式解释,即在运行时输入的代码。我同意,但是,这不是问题的范围,因为它不是语言的功能。
拉斐尔

@DavidRicherby和Raphael,我说这篇文章的作者暗示(我在回答中描述的内容)是自我修改功能,这当然是设计语言构造的,而不是某些特定程序的人工
产物
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.