这是一个关于宏的理论问题(我认为)。我知道宏可以获取源代码并生成目标代码而无需对其进行评估,从而使程序员能够创建更多功能的语法结构。如果我必须对这两个宏系统进行分类,我会说有“C风格”宏和“Lisp风格”宏。
调试宏似乎有点棘手,因为在运行时,实际运行的代码与源代码不同。
调试器如何根据预处理的源代码跟踪程序的执行情况?是否有特殊的“调试模式”必须设置为捕获有关宏的额外数据?
在C中,我可以理解你为调试设置了一个编译时开关,但是解释语言(如某些形式的Lisp)会如何做呢?
为不尝试这一点道歉,但是lisp工具链需要的时间比我花费的时间要长。
答案 0 :(得分:3)
我不认为“C风格”和“Lisp风格”宏在编译方式上存在根本区别。两者都在编译器正确看到它之前转换源。最大的区别在于C的宏使用C预处理器(一种较弱的辅助语言,主要用于简单的字符串替换),而Lisp的宏是用Lisp本身编写的(因此可以做任何事情)。
(顺便说一下:我有一段时间没见过一个非编译的Lisp ...当然不是世纪之交。但是如果有的话,被解释似乎会让宏调试问题更容易,不是更难,因为你有更多的信息。)
我同意Michael的观点:我还没有看到C的调试器处理宏。在发生任何事情之前,使用宏的代码会被转换。 "debug" mode for compiling C code通常只意味着存储functions, types, variables, filenames, and such - 我认为它们中没有任何存储有关宏的信息。
用于调试使用的程序 宏,Lisp几乎是一样的 在这里作为C:你的调试器看到了 编译代码,而不是宏 应用。通常是宏 保持简单,并调试 使用前独立,避免 需要这个,就像C。
用于调试宏
自己,在你去某处使用它之前,Lisp确实有功能
这使得这比C更容易,
例如,repl和
macroexpand-1
(虽然在C中
显然有办法
macroexpand整个文件,完全,在
一旦)。你可以看到
宏观扩张之前和之后,
在你的编辑中,当你写作时
它
我记不起任何时候遇到调试进入宏定义本身会很有用的情况。要么它是宏定义中的一个错误,在这种情况下macroexpand-1
立即隔离问题,或者它是一个下面的错误,在这种情况下,正常的调试工具工作正常,我不在乎两个之间发生宏扩展我的电话堆栈的框架。
答案 1 :(得分:3)
在LispWorks中,开发人员可以使用Stepper tool。
LispWorks提供了一个步进器,可以逐步完成整个macro expansion process。
答案 2 :(得分:2)
您应该真正研究Racket对使用宏调试代码的支持。肯提到,这种支持有两个方面。一方面存在调试宏的问题:在Common Lisp中,最好的方法是手动扩展宏窗体。对于CPP,情况类似但更原始 - 您只通过CPP扩展运行代码并检查结果。但是,对于更多涉及的宏来说,这两者都是不够的,这就是在Racket中拥有macro debugger的动机 - 它会逐个显示语法扩展步骤,并为绑定等事项提供额外的基于gui的指示标识符等。
在使用宏的一面,Racket一直比其他Scheme和Lisp实现更先进。这个想法是每个表达式(作为语法对象)是代码加上包含其源位置的附加数据。这种方式当一个表单是一个宏时,具有来自宏的部分的扩展代码将具有正确的源位置 - 从宏的定义而不是从其使用(表单实际上不存在)。一些Scheme和Lisp实现将使用子表单的标识实现对此的限制,如dmitry-vk所述。
答案 3 :(得分:1)
我不知道lisp宏(我怀疑它可能与C宏完全不同)或调试,但很多 - 可能是大多数 - C / C ++调试器不能很好地处理C预处理器宏的源代码级调试。
通常,C / C ++调试器不会“进入”宏定义。如果宏扩展为多个语句,那么调试器通常只会停留在每个调试器“步骤”操作的同一源代码行(调用宏的位置)。
这可能会使调试宏比其他情况下更加痛苦 - 这是在C / C ++中避免使用它们的另一个原因。如果一个宏以一种真正神秘的方式行为不端,我将进入汇编模式进行调试或扩展宏(手动或使用编译器的开关)。你必须走到这个极端是非常罕见的;如果您正在编写那些复杂的宏,那么您可能采取了错误的方法。
答案 4 :(得分:1)
通常在C源代码级调试中具有行粒度(“next”命令)或指令级粒度(“step into”)。宏处理器在处理源中插入特殊指令,允许编译器将编译的CPU指令序列映射到源代码行。
在Lisp中,宏和编译器之间不存在跟踪源代码到编译代码映射的约定,因此在源代码中并不总是可以单步执行。
明显的选择是在宏扩展代码中单步执行。编译器已经看到最终的,扩展的代码版本,并且可以跟踪源代码到机器代码映射。
其他选项是使用操作期间lisp表达式具有标识的事实。如果宏很简单,只是解构并将代码粘贴到模板中,那么扩展代码的某些表达式(与EQ比较相同)与从源代码读取的表达式相同。在这种情况下,编译器可以将一些表达式从扩展代码映射到源代码。
答案 5 :(得分:0)
简单的答案是它很复杂;-)有几个不同的东西有助于调试程序,甚至更多用于跟踪宏。
在C和C ++中,预处理器用于扩展宏并包含在实际的源代码中。使用#line指令在此扩展的源文件中跟踪原始文件名和行号。
http://msdn.microsoft.com/en-us/library/b5w2czay(VS.80).aspx
在启用调试的情况下编译C或C ++程序时,汇编程序会在目标文件中生成跟踪源行,符号名称,类型描述符等的附加信息。
http://sources.redhat.com/gdb/onlinedocs/stabs.html
操作系统具有使调试器可以附加到进程并控制进程执行的功能;暂停,单步等等。
当调试器附加到程序时,它会通过在调试信息中查找程序地址的含义,将进程堆栈和程序计数器转换回符号形式。
动态语言通常在虚拟机中执行,无论是解释器还是字节码VM。 VM提供挂钩以允许调试器控制程序流并检查程序状态。