【问题标题】:How to implement a call stack in JavaScript?如何在 JavaScript 中实现调用堆栈?
【发布时间】:2020-08-25 08:53:06
【问题描述】:

调用堆栈由堆栈帧(也称为激活记录或激活帧)组成。

假设我有代码(一种自定义语言),它被编译成 AST(在 JavaScript 中)。语言示例如下所示:

fun a(z):
  let x = 10
  let y = z * x
  let w = b(y, x)
  return w
fun b(a, b):
  let x = c(a, 10)
  let y = c(b, 20)
  let z = d(x, y)
  return z
fun c(a, b):
  let x = a + 5
  let y = b + 7
  let z = d(x, y)
  return z
fun d(a, b):
  return a * b
main:
  a(argv[0])

不要作弊并说明如何静态编译掉它或其他什么,说你只是为此创建一个解释器。

您将如何创建调用堆栈?每个激活记录具体是什么样的? (在 JS 实现中)。

假设我们有一个您可以从源代码中想象的简单 AST:

{
  type: 'fun',
  name: 'a',
  params: ['z'],
  steps: [
    {
      type: 'let',
      name: 'z',
      value: {
        type: 'call',
        name: 'd',
        params: ['x', 'y']
      }
    },
    ...
  ]
}

那么你要解释那个AST:

let pointer = 'main'
let callstack = []
let stack = [funs[pointer]]
while (stack.length) {
  let current = stack.shift()
  current.steps.forEach(step => {
    if (step.type == 'call') {
      // PUSH ACTIVATION RECORD SOMEHOW
      step.params.forEach(param => {
        callstack.push(param) // how to reach value from reference?
      })
      stack.push(step)
    }
  })
}

我完全迷路了。那个解释函数会是什么样子?

【问题讨论】:

  • 如果我正在编写一个 AST-walking 解释器,我很可能不会实现调用堆栈,而只是使用宿主语言的堆栈。我觉得实现自己的堆栈有意义的大多数场景也需要平面 IR(即某种形式的字节码,而不是树)。除此之外,您如何表示堆栈取决于您的语言要求。例如,如果您拥有指针的全部功能,您可能需要一个字节数组来表示您的内存,而堆栈只是其中的一部分。
  • @sepp2k 实现调用堆栈的语言是 JavaScript。想象一下,我们将它编译为 3 地址代码而不是 AST,然后呢?
  • “实现调用堆栈的语言是 JavaScript” 当我说“你的语言的要求”时,我指的是你正在实现的语言,而不是宿主语言。就像您正在为具有指针的语言编写解释器一样,您需要字节数组作为内存,因此您可以将指针实现为该数组的索引。 “想象一下,我们将它编译为 3 地址代码而不是 AST,然后呢?”然后您可以通过推送一个包含当前指令索引 +1 作为返回地址的新堆栈帧来实现调用,并将 return 实现为pc = stack.pop().ret;
  • "那你就可以实现了..." 我知道要做什么,只是不知道具体怎么做!

标签: javascript compiler-construction virtual-machine interpreter


【解决方案1】:

好的,首先,让我们为您的示例构建正确的 AST。在这种情况下,正确获取细节非常重要。它应该看起来更像这样,但这远非完美的 AST。

{ type: compilation_unit // this is an object that is an array of functions
  [ 
    { type: fun // this is what you will push on your 'call stack'
      name: 'a'
      params: ['z']
      locals: ['x', 'y', 'w']
      steps: [
        { type: let
          name: 'x'
          value: { type: const
                   value: 10
                 }
         },
         { type: let
           name: 'y'
           value: { type: '*'
                    left: { type: var
                            name: 'z'
                          }
                     right: { type: var
                              name: 'x'
                             }
                   }
           }
    ...
    }
    { type: fun
      name: 'b'
      ...
    }
    ...
    { type: fun
      name: 'main'
      params ...
      locals ...
      steps: [
        { type: call
          name: 'a'
          args: [ 
             { type: '[]'
               name: 'argv'
               subscript: {
                  { type: const
                    value: 0
                  }
             }
           ]
        }
      ]
    } // type fun
  ]
}

因此,如果您使用 AST 作为 IR,那么当您到达调用“节点”时,您压入堆栈的内容就是您正在调用的函数的 [指向] AST 的指针。这就是你需要的信息。参数、局部变量和您需要执行的步骤都在那里。您的字节码解释器(在本例中为 AST 步行解释器)只需要查看类型:有趣的对象并查看有多少参数、多少本地变量等,并为它们分配空间。

但是,这可能不是您想要的。您谈论 3 地址代码,这通常意味着较低级别的 IR。为此,您需要一个将 AST 转换为较低级别表示的阶段。您需要通过 AST 以生成“较低级别”的 IR。

我个人会推荐 LLVM IR 之类的东西,但这需要你做一些研究。但是要查看这样的堆栈帧是什么样子,请找到一个生成 LLVM IR 的 C 编译器(或其他一些与您想要的语言接近(r)的编译器)并查看它为调用指令和堆栈帧生成的内容.然后,您可以将该类型:有趣的 AST 对象转换为该指令序列。

【讨论】:

    猜你喜欢
    • 2019-08-03
    • 2020-04-11
    • 2010-09-22
    • 2021-10-11
    • 1970-01-01
    • 2021-06-24
    • 2013-12-18
    • 2021-06-05
    • 1970-01-01
    相关资源
    最近更新 更多