什么时候先将自己的语言编译为C代码有意义?


34

在设计自己的编程语言时,什么时候编写一个将源代码转换为C或C ++代码的转换器,以便我可以使用现有的编译器(如gcc)来生成机器代码?是否有使用这种方法的项目?



4
如果您过去使用C,您会发现C#和Java都可以编译为中间语言。您不必针对其他中间语言,而不必直接进行汇编,而不必重做别人已经完成的许多工作。
Casey 2014年

1
@emodendroket但是,C#和Java编译为通常被设计为IL且专门针对C#/ Java的IL,因此与IL相比,CIL和JVM字节码在许多方面都比C更明智和方便。这与是否使用任何中间语言无关,而与要使用哪种中间语言有关。

1
查看几种生成C代码的免费软件实现。我希望您能使您的语言实现免费软件。
Basile Starynkevitch 2014年

2
这是@RobertHarvey评论的更新链接:yosefk.com/blog/c-as-an-intermediate-language.html
基督教教务长

Answers:


52

转换为C代码是一个非常成熟的习惯。带有类的原始C(以及早期的C ++实现,然后称为Cfront)成功地做到了这一点。Lisp或Scheme的几种实现正在执行此操作,例如Chicken SchemeScheme48Bigloo。有些人翻译的Prolog到CMozart的某些版本也是如此(并且已经尝试将Ocaml字节码编译为C)。J.Pitrat的人工智能CAIA系统也被引导并生成其所有C代码。对于GTK相关的代码,Vala还转换为C。Queinnec的书Lisp Small Pieces 有一些关于翻译成C的章节。

转换为C时的问题之一是尾递归调用。即使在某些情况下,最新版本的GCC(或Clang / LLVM)进行了这种优化,C标准也不保证C编译器会正确地翻译它们(转换为“带有参数的跳转”,即占用调用堆栈)。。

另一个问题是垃圾收集。几个实现只使用Boehm保守垃圾收集器(对C友好 ...)。如果您想垃圾收集代码(像一些Lisp实现一样,例如SBCL),那可能是一场噩梦(您想dlclose在Posix上使用)。

另一个问题是处理一流的延续call / cc。但是可以使用一些巧妙的技巧(在Chicken Scheme中查看)。访问调用栈可能需要很多技巧(但请参见GNU backtrace等。)。在C中,连续性(即堆栈或线程)的正交持久性将很困难。

异常处理通常是向longjmp等发出聪明的调用的问题。

您可能想要生成(在发出的C代码中)适当的#line指令。这很无聊,并且需要大量工作(您可能希望这样做例如生成更易于gdb调试的代码)。

我的MELT lispy域特定语言(用于自定义或扩展GCC)被翻译成C(实际上现在是糟糕的C ++)。它具有自己的分代复制垃圾收集器。(您可能会对QishRavenbrook MPS感兴趣)。实际上,用计算机生成的C代码比使用手写C代码更容易生成世代GC(因为您将为写屏障和GC机械量身定制C代码生成器)。

我不知道有任何语言实现可转换为真正的 C ++代码,即使用一些“编译时垃圾收集”技术来使用许多STL模板并尊重RAII习惯来发出C ++代码。(请告诉您是否知道)。

今天有趣的是,C编译器(在当前的Linux桌面上)可能足够快,可以实现转换为C 的交互式顶级read-eval-print-loop:您将在每个用户处发出C代码(几百行)交互,fork则将其编译为共享对象,然后将其共享dlopen。(MELT已准备就绪,通常足够快)。所有这些可能花费十分之几秒的时间,并且最终用户可以接受。

如果可能,我建议您转换为C,而不是C ++,尤其是因为C ++编译速度很慢。

如果要实现您的语言,则可能还会考虑(而不是发出C代码)某些JIT库,例如libjit,GNU lightningasmjit甚至LLVMGCCJIT。如果要转换为C,有时可能使用tinycc:它会非常快速地编译生成的C代码(即使在内存中),从而减慢了机器代码的速度。但总的来说,您想利用由真正的C编译器(如GCC)完成的优化

如果将您的语言翻译成C,请确保首先在内存中构建生成的C代码的整个AST(这也使得首先生成所有声明,然后生成所有定义和功能代码更加容易)。您将可以通过这种方式进行一些优化/标准化。另外,您可能对几个GCC扩展感兴趣(例如,计算机Gotos)。您可能要避免生成庞大的 C函数-例如,生成十万行C的代码-(最好将它们拆分成较小的部分),因为优化C编译器对大型C函数非常不满意(实际上,实验上gcc -O大型函数的编译时间与函数代码大小的平方成正比。因此,将每个生成的C函数的大小限制为几千行。

请注意,Clang(通过LLVM)和GCC(通过libgccjit)C&C ++编译器都提供了某种方式来生成一些适合于这些编译器的内部表示形式,但这样做可能(或不会)比发出C(或C ++)代码更难,并且特定于每个编译器。

如果设计要翻译为C的语言,则可能需要一些技巧(或构造)来生成C与您的语言的混合体。我在DSL2011上发表的论文《MELT: 嵌入在GCC编译器中的翻译领域特定语言》应该为您提供有用的提示。


您是指“鸡肉计划”吗?
罗伯特·哈维

1
是的,我提供了URL。
Basile Starynkevitch 2014年

使虚拟机(例如Java或类似的东西)将字节码编译为C,然后使用gcc进行JIT编译是否相对可行?还是应该直接从字节码转换为汇编?
Panzercrisis 2014年

1
@Panzercrisis大多数JIT编译器都需要其机器代码后端来支持诸如替换功能和使用跳转/陷阱门修补现有代码之类的事情。除此之外,gcc特别是...在体系结构上不适合JIT编译和其他用例。不过请查看libgccjit:gcc.gnu.org/ml/gcc-patches/2013-10/msg00228.htmlgcc.gnu.org/wiki/JIT

1
很好的定位材料。谢谢!
capr

7

当生成完整机器代码的时间超过使用C编译器将“ IL”编译为机器代码的中间步骤所带来的不便时,这才有意义。

通常,领域特定语言是用这种方式编写的,非常高级的系统用于定义或描述一个过程,然后将其编译成可执行文件或dll。产生有效/良好的汇编所花费的时间比产生C所花费的时间要多得多,并且C与汇编代码在性能上非常接近,因此产生C并重用C编译器作者的技能是很有意义的。请注意,它不仅是编译,而且还在进行优化-编写gcc或llvm的人花了很多时间来制作优化的机器代码,试图重新发明他们的所有辛苦工作是愚蠢的。

重复使用IIRC与语言无关的LLVM的编译器后端可能更容易接受,因此您生成LLVM指令而不是C代码。


似乎库也是考虑它的一个非常有说服力的理由。
Casey 2014年

当您说“您的'IL'”时,您指的是什么?抽象语法树?
罗伯特·哈维

@RobertHarvey不,我的意思是C代码。对于OP,这是他自己的高级语言和机器代码之间的中间语言。我将其引号中的内容试图传达这种想法,即它不是许多人使用的IL(例如,Microsoft的.NET IL)
gbjbaanb 2014年

2

编写编译器来生成机器代码可能不会比编写产生C的编译器困难(在某些情况下可能会更容易),但是生成机器代码的编译器将只能在特定平台上生成可运行程序。它是书面的;相比之下,生成C代码的编译器可能能够为使用C的方言的任何平台生成程序,生成的代码旨在支持C的方言。请注意,在许多情况下,有可能编写完全可移植的C代码,并且无需使用C标准无法保证的任何行为即可按要求运行,但是依赖于平台保证的行为的代码可能能够运行得更快在做出那些保证的平台上,比没有做出保证的平台上。

例如,假设一种语言支持一种功能,该功能可UInt32UInt8[]以大端顺序方式解释的任意对齐的四个连续字节中产生a 。在某些编译器上,可以将代码编写为:

uint32_t dat = *(__packed uint32_t*)p;
return (dat >> 24) | (dat >> 8) | ((uint32_t)dat << 8) | ((uint32_t)dat << 24));

并使编译器生成一个字加载操作,后跟一个字中的反字节指令。但是,某些编译器不支持__packed修饰符,并且在缺少该修饰符的情况下会生成无法正常工作的代码。

或者,可以将代码编写为:

return dat[3] | ((uint16_t)dat[2] << 8) | ((uint32_t)dat[1] << 16) | ((uint32_t)dat[0] << 24);

这样的代码应该可以在任何平台上运行,即使CHAR_BITS不是8平台(假设源数据的每个八位位组都以不同的数组元素结尾),但是这样的代码运行速度可能不如非便携式平台快。版本在支持前者的平台上。

请注意,可移植性通常要求代码具有类型转换和类似构造的自由度。例如,为了可移植性,想要将两个32位无符号整数相乘并产生结果的低32位的代码必须编写为:

uint32_t result = 1u*x*y;

如果没有1u,一个系统,其中INT_BITS介于33到64可以合法地做任何事情对一个编译器,它希望x的,如果和y的乘积超过2,147,483,647较大,和一些编译器很容易采取这种机会。


1

您在上面有一些很好的答案,但是鉴于您在一条评论中回答了“为什么要首先创建自己的编程语言?”的问题,并回答“主要是出于学习目的,”我将从另一个角度回答。

编写一个接受源代码并将其转换为C或C ++代码的转换器是有意义的,因此,如果您对学习词法,语法和语法更感兴趣,则可以使用gcc之类的现有编译器来生成机器代码。语义分析比您学习代码生成和优化要好!

编写自己的机器代码生成器是一项非常重要的工作,如果您不是最感兴趣的,则可以通过编译为C代码来避免!

但是,如果您沉迷于汇编程序并且对在最低级别上优化代码的挑战着迷,那么一定要自己编写一个代码生成器以获取学习经验!


-7

如果您使用的是Windows,则取决于所使用的操作系统,Microsoft IL(中间语言)可以将您的代码转换为中间语言,从而无需花费任何时间即可将其编译为机器代码。或者,如果您使用的是Linux,则可以使用单独的编译器

回到您的问题是,当您设计自己的语言时,您应该为此使用单独的编译器或解释器,因为机器不了解高级语言。您的代码应编译为机器代码,以使其对机器有用


2
Your code should be compiled into machine code to make it useful for machine-如果您的编译器生成c代码作为输出,则可以将c代码放入ac编译器以生成机器代码,对吗?
罗伯特·哈维

是。因为机器没有C语言
Tayyab Gulsher沃赫拉

2
对。因此,问题是“什么时候发出c并使用ac编译器而不是直接发出机器语言或字节码才有意义?”
罗伯特·哈维

实际上,他要求设计自己的编程语言,在这种编程语言中,他要求“将其转换为C或C ++代码”。因此,如果您正在设计自己的编程语言,那么我在解释这一点,为什么您应该使用c编译器或c ++。如果您足够聪明,则应该设计自己的
Tayyab Gulsher Vohra 2014年

8
我认为您不理解这个问题。参见yosefk.com/blog/c-as-an-intermediate-language.html
Robert Harvey
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.