Compiler construction 闭包转换和高阶函数调用的单独编译

Compiler construction 闭包转换和高阶函数调用的单独编译,compiler-construction,programming-languages,functional-programming,closures,Compiler Construction,Programming Languages,Functional Programming,Closures,在编译高阶函数调用时,是否有处理单独编译和不同类型闭包转换之间交互的标准方法 我知道在大多数编程语言中都有三种类似函数的结构:闭包、(顶级)函数和C++风格的函数对象。从语法上讲,它们的调用方式相同,但编译器会以最佳方式生成形状明显的调用站点: Syntax: | clo(args) | func(args) | obj(args) ---------------------------------------------------------

在编译高阶函数调用时,是否有处理单独编译和不同类型闭包转换之间交互的标准方法

我知道在大多数编程语言中都有三种类似函数的结构:闭包、(顶级)函数和C++风格的函数对象。从语法上讲,它们的调用方式相同,但编译器会以最佳方式生成形状明显的调用站点:

Syntax:  | clo(args)                 |   func(args)     |   obj(args)
--------------------------------------------------------------------------------
Codegen: | clo.fnc(&clo.env, args)   |   func(args)     |   cls_call(&obj, args)
              ^      ^                      ^                   ^     ^
            fn ptr   |                      +--"top level" fn --+     |
                     +--- "extra" param, compared to source type -----+
<>(在C++中,代码> CLSYLIDE/<代码>将是<代码> >:操作程序()/<代码> <代码> Obj>代码>类>代码>,C++还允许虚拟函子,但这基本上是一个额外的间接关闭情况。 此时,调用
map(x=>x>3)lst
map(x=>x>y)lst
应该调用不同的
map
函数,因为第一个函数是提升后的简单函数指针,第二个函数是闭包

我可以想出四种方法来处理这个问题:

< L> > P> C++,98方法,它强制被调用方选择一个调用站点形状(通过形式参数类型:虚拟函子、函数指针或非虚函子)或使用模板删除单独编译,有效地指定下面的解2。

  • 重载:编译器可以对
    映射
    和所有其他高阶函数进行多个实例化,并使用适当的名称进行修改。实际上,每个调用站点形状都有一个单独的内部函数类型,重载解析会选择正确的类型

  • 强制使用全球统一的呼叫站点形状。这意味着所有顶级函数都采用显式
    env
    参数,即使它们不需要它,并且必须引入“额外”闭包来包装非闭包参数

  • 保留顶级函数的“自然”签名,但要求所有高阶函数参数的处理都通过闭包完成。已关闭函数的“额外”闭包调用包装蹦床函数以丢弃未使用的
    env
    参数。这似乎比选项3更优雅,但更难有效实施。编译器要么生成大量调用约定独立包装器,要么使用少量调用约定敏感包装器

  • 拥有一个优化的闭包转换/lambda提升混合方案,每个函数可以选择是将给定的闭包参数粘贴在env还是参数列表中,这似乎会使问题更加尖锐

    无论如何,问题是:

    • 这个问题在文献中有明确的名称吗
    • 除了上述四种方法外,还有其他方法吗
    • 方法之间是否存在众所周知的权衡

    这是一个相当深刻的问题,有很多影响,我不想在这里写学术文章。我将只是简单地介绍一下,并向您介绍其他地方的更多信息。我的回答是基于我个人对和的经验,以及关于这些系统的学术论文

    在一个雄心勃勃的编译器中,关键的区别在于已知调用和未知调用之间的区别。对于具有高阶函数的语言,第二个但仍然重要的区别是调用是否完全饱和(我们只能在已知的调用位置确定)

    • 已知调用表示一个调用站点,在该站点中,编译器确切地知道调用什么函数,以及它需要多少参数

    • 未知调用意味着编译器无法确定可能调用的函数

    • 如果被调用的函数获得了它所期望的所有参数,并且直接进行编码,则已知调用是完全饱和的。如果函数得到的参数比预期的少,则该函数将部分应用,并且调用只会分配闭包

    例如,如果我编写Haskell函数

    mapints :: (Integer -> a) -> [a]
    mapints f = map f [1..]
    
    然后对
    map
    的调用是已知的,并且完全饱和。
    如果我写

    inclist :: [Integer] -> [Integer]
    inclist = map (1+)
    
    然后对
    map
    的调用是已知的,并且部分应用。
    最后,如果我写

    compose :: (b -> c) -> (a -> c) -> (a -> c)
    compose f g x = f (g x)
    
    然后对
    f
    g
    的调用都是未知的

    成熟的编译器所做的主要工作是优化已知调用。在您上面的分类中,此策略主要属于#2

    • 如果一个函数的所有调用位置都是已知的,那么一个好的编译器将为该函数创建一个专用的调用约定,例如,在正确的寄存器中传递参数以使事情顺利进行

    • 如果某个函数的某些调用位置已知,但并非所有调用位置都已知,编译器可能会认为值得为已知调用创建一个特殊用途的调用约定,该约定可以是内联的,也可以使用只有编译器知道的特殊名称。在源代码中以名称导出的函数将使用标准调用约定,它的实现通常是对专用版本进行优化尾部调用的薄层

    • 如果一个已知调用没有完全饱和,编译器只会生成代码,在调用方中分配闭包

    闭包的表示(或者一类函数是否由lambda提升或去功能化等其他技术处理)在很大程度上与已知调用和未知调用的处理是正交的

    (值得一提的是另一种方法:它是一个完整的程序编译器;它可以查看所有的源代码;它使用一种我已经忘记的技术将所有函数简化为一阶。仍然存在未知调用,因为高阶语言中的一般控制流分析很难处理。)


    关于你最后的问题