编程语言如何定义函数?


28

编程语言如何定义和保存函数/方法?我正在用Ruby创建一种解释型编程语言,并且试图弄清楚如何实现函数声明。

我的第一个想法是将声明的内容保存在地图中。例如,如果我做了类似的事情

def a() {
    callSomething();
    x += 5;
}

然后,我要在地图中添加一个条目:

{
    'a' => 'callSomething(); x += 5;'
}

这样做的问题是它将变得递归,因为我必须parse在字符串上调用我的方法,该方法在parse遇到字符串时会再次调用doSomething,然后最终将耗尽堆栈空间。

那么,解释型语言如何处理呢?


哦,这是我关于Programmers.SE的第一篇文章,所以如果我做错任何事情或这是题外话,请通知我。:)
门把手

过去,我将它们全部内联存储在我的令牌中,而函数调用只是跳转到特定的偏移量(很像Assembly中的标签)。您要标记脚本吗?还是每次都解析字符串?
西蒙·怀特海德2013年

@SimonWhitehead我将字符串拆分为令牌,然后分别解析每个令牌。
Doorknob 2013年

3
如果您不熟悉编程语言的设计和实现,则可能需要查看有关该主题的一些文献。最受欢迎的一本是《龙书》:en.wikipedia.org/wiki/…,但是还有其他更简洁的文本也很好。例如,可以在这里免费获得Aarne Ranta的《实现编程语言》:bit.ly/15CF6gC
evilcandybag 2013年

1
@ddyer谢谢!我用不同的语言谷歌搜索了一个口齿不清的口译员,这确实有所帮助。:)
门把手

Answers:


31

假设您的“解析”函数不仅解析代码,而且可以同时执行代码,我是否正确?如果您想这样做,则不要在地图中存储函数的内容,而要存储函数的位置

但是有更好的方法。预先需要付出更多的努力,但是随着复杂度的增加,它会产生更好的结果:使用抽象语法树。

基本思想是,您永远只能解析一次代码。然后,您有一组表示操作和值的数据类型,并制作了它们的树,如下所示:

def a() {
    callSomething();
    x += 5;
}

变成:

Function Definition: [
   Name: a
   ParamList: []
   Code:[
      Call Operation: [
         Routine: callSomething
         ParamList: []
      ]
      Increment Operation: [
         Operand: x
         Value: 5
      ]
   ]
]

(这只是假设的AST结构的文本表示形式。实际的树可能不是文本形式。)无论如何,您将代码解析为AST,然后直接在AST上运行解释器,或使用第二遍(“代码生成”)将AST转换为某种输出形式。

就您的语言而言,您可能要做的是创建一个映射,该映射将函数名称映射到函数AST,而不是将函数名称映射到函数字符串。


好的,但是问题仍然存在:它使用递归。如果这样做,最终将耗尽堆栈空间。
Doorknob 2013年

3
@Doorknob:具体使用递归是什么?任何块结构的编程语言(比ASM 都高的现代语言)都是基于树的,因此本质上是递归的。您担心哪个特定方面会使堆栈溢出?
梅森惠勒2013年

1
@Doorknob:是的,这是任何语言的固有属性,即使它已被编译成机器代码。(调用堆栈就是这种行为的体现。)实际上,我是按照我所描述的方式工作的脚本系统的贡献者。和我一起聊天,位于chat.stackexchange.com/rooms/10470/…,我将与您讨论一些有效解释的技术,并将对堆栈大小的影响最小化。:)
梅森惠勒

2
@Doorknob:这里没有递归问题,因为AST中的函数调用按名称引用了函数,不需要引用实际函数。如果要编译为机器代码,那么最终将需要函数地址,这就是大多数编译器进行多次传递的原因。如果要使用一遍编译器,则需要所有函数的“前向声明”,以便编译器可以预先分配地址。字节码编译器甚至不必为此烦恼,抖动会处理名称查找。
亚罗诺(Aaronaught)2013年

5
@Doorknob:确实是递归的。是的,如果您的堆栈只有16个条目,则将无法解析(((((((((((((((( x )))))))))))))))))。实际上,堆栈可能更大,并且实际代码的语法复杂性非常有限。当然,如果该代码必须是人类可读的。
MSalters 2013年

4

看到后,您不应该调用parse callSomething()(我想您的意思callSomething不是doSomething)。a和之间的区别在于callSomething,一个是方法定义,而另一个是方法调用。

看到新定义时,您将需要进行检查以确保可以添加该定义,因此:

  • 检查功能是否还不具有相同的签名
  • 确保在适当的范围内执行方法声明(即可以在其他方法声明中声明方法吗?)

假设这些检查通过,则可以将其添加到地图中并开始检查该方法的内容。

找到类似的方法调用时callSomething(),应执行以下检查:

  • callSomething您的地图中是否存在?
  • 是否正确调用了它(参数数量与找到的签名匹配)?
  • 参数是否有效(如果使用了变量名,是否声明了它们?可以在此作用域中访问它们吗?)?
  • 可以从您所在的地方(在私人,公共,受保护的地方)呼叫callSomething吗?

如果您认为callSomething()还可以,那么此时您真正想做的事情取决于您希望采用的方式。严格来说,一旦您知道这样的调用已经可以了,您就只能保存方法的名称和参数,而无需进一步的细节。运行程序时,将使用在运行时应具有的参数来调用该方法。

如果想更进一步,您不仅可以保存字符串,还可以保存指向实际方法的链接。这样会更有效,但是如果您必须管理内存,可能会造成混乱。我建议您先简单地抓住字符串。稍后您可以尝试进行优化。

请注意,所有这些假设都是假设您已经对程序进行了lexx处理,这意味着您已经识别了程序中的所有标记并知道它们什么。这并不是说您知道它们是否一起有意义,这就是解析阶段。如果您还不知道令牌是什么,我建议您首先专注于首先获取该信息。

希望对您有所帮助!欢迎来到程序员SE!


2

在阅读您的帖子时,我注意到您的问题中有两个问题。最重要的是如何解析。有多种解析器(例如递归下降解析器LR解析器Packrat解析器)和解析器生成器(例如GNU bisonANTLR),可以使用给定(显式或隐式)语法“递归”遍历文本程序。

第二个问题是关于函数的存储格式。当您不进行语法指导的翻译时,可以创建程序的中间表示形式,该中间表示形式可以是抽象语法树或某些自定义的中间语言,以便对其进行进一步处理(编译,转换,执行,编写)文件等)。


1

从一般的角度来看,函数的定义只不过是代码中的标签或书签。其他大多数循环,作用域和条件运算符都相似。它们是较低抽象层中基本“跳转”或“转到”命令的替代品。函数调用基本上可以归结为以下低层计算机命令:

  • 将所有参数的数据以及指向当前函数的下一条指令的指针连接到一个称为“调用堆栈帧”的结构中。
  • 将此框架推入调用堆栈。
  • 跳转到函数代码第一行的内存偏移量。

然后,“ return”语句或类似语句将执行以下操作:

  • 将要返回的值加载到寄存器中。
  • 将指向调用方的指针加载到寄存器中。
  • 弹出当前堆栈框架。
  • 跳到呼叫者的指针。

因此,功能只是高级语言规范中的抽​​象,它使人们可以以更易于维护和直观的方式组织代码。当被编译为一种汇编语言或中间语言(JIL,MSIL,ILX)时,并且无疑当以机器代码呈现时,几乎所有此类抽象都将消失。

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.