在设计自己的编程语言时,什么时候编写一个将源代码转换为C或C ++代码的转换器,以便我可以使用现有的编译器(如gcc)来生成机器代码?是否有使用这种方法的项目?
在设计自己的编程语言时,什么时候编写一个将源代码转换为C或C ++代码的转换器,以便我可以使用现有的编译器(如gcc)来生成机器代码?是否有使用这种方法的项目?
Answers:
转换为C代码是一个非常成熟的习惯。带有类的原始C(以及早期的C ++实现,然后称为Cfront)成功地做到了这一点。Lisp或Scheme的几种实现正在执行此操作,例如Chicken Scheme,Scheme48,Bigloo。有些人翻译的Prolog到C。Mozart的某些版本也是如此(并且已经尝试将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 ++)。它具有自己的分代复制垃圾收集器。(您可能会对Qish或Ravenbrook 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 lightning,asmjit甚至LLVM或GCCJIT。如果要转换为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编译器中的翻译领域特定语言》应该为您提供有用的提示。
当生成完整机器代码的时间超过使用C编译器将“ IL”编译为机器代码的中间步骤所带来的不便时,这才有意义。
通常,领域特定语言是用这种方式编写的,非常高级的系统用于定义或描述一个过程,然后将其编译成可执行文件或dll。产生有效/良好的汇编所花费的时间比产生C所花费的时间要多得多,并且C与汇编代码在性能上非常接近,因此产生C并重用C编译器作者的技能是很有意义的。请注意,它不仅是编译,而且还在进行优化-编写gcc或llvm的人花了很多时间来制作优化的机器代码,试图重新发明他们的所有辛苦工作是愚蠢的。
重复使用IIRC与语言无关的LLVM的编译器后端可能更容易接受,因此您生成LLVM指令而不是C代码。
编写编译器来生成机器代码可能不会比编写产生C的编译器困难(在某些情况下可能会更容易),但是生成机器代码的编译器将只能在特定平台上生成可运行程序。它是书面的;相比之下,生成C代码的编译器可能能够为使用C的方言的任何平台生成程序,生成的代码旨在支持C的方言。请注意,在许多情况下,有可能编写完全可移植的C代码,并且无需使用C标准无法保证的任何行为即可按要求运行,但是依赖于平台保证的行为的代码可能能够运行得更快在做出那些保证的平台上,比没有做出保证的平台上。
例如,假设一种语言支持一种功能,该功能可UInt32
从UInt8[]
以大端顺序方式解释的任意对齐的四个连续字节中产生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较大,和一些编译器很容易采取这种机会。
如果您使用的是Windows,则取决于所使用的操作系统,Microsoft IL(中间语言)可以将您的代码转换为中间语言,从而无需花费任何时间即可将其编译为机器代码。或者,如果您使用的是Linux,则可以使用单独的编译器
回到您的问题是,当您设计自己的语言时,您应该为此使用单独的编译器或解释器,因为机器不了解高级语言。您的代码应编译为机器代码,以使其对机器有用
Your code should be compiled into machine code to make it useful for machine
-如果您的编译器生成c代码作为输出,则可以将c代码放入ac编译器以生成机器代码,对吗?