What does the call stack look like in a system of nested functions, functions passed as arguments, and returned functions?

时间秒杀一切 提交于 2021-02-08 06:33:13

问题


Here is a made up example of nested functions with functions passed as parameters and functions returned:

function foo() {
  let x = 100
  function bar(a, b) {
    function mul(cb) {
      let m = x * a
      cb(m)
    }

    function add(cb) {
      let m = x + b
      cb(m)
    }

    function update(m) {
      x = m
    }
    
    mul(update)
    add(update)

    console.log(x)
  }
  return bar
}

let bar = foo()
bar(2, 3) // => 203
bar(2, 3) // => 409
bar(2, 3) // => 821
bar(2, 3) // => 1645

Without figuring out potential ways in which these functions could be optimized and/or reduced/simplified by a crafty compiler, how would a naive compiler represent these in a call stack with activation records?

  • bar is a nested function returned from the parent function. It uses x from the parent scope, even though the parent scope is now "closed".
  • mul uses x from the parent-parent scope, and a from the parent scope. In addition, it takes a callback cb which is a function that updates x.
  • add is similar to mul.

I have been reading online about how activation records work, and get how to implement them in JavaScript (in a JavaScript VM) using a simple stack. However, I haven't yet found any good resources explaining how to implement nested functions and functions as parameters like the example I posted above. It seems these are related to non-local variables, but there isn't much information on these. Same with nested functions. For example, Wikipedia says this:

the language runtime code must also implicitly pass the environment (data) that the function sees inside its encapsulating function, so that it is reachable also when the current activation of the enclosing function no longer exists. This means that the environment must be stored in another memory area than (the subsequently reclaimed parts of) a chronologically based execution stack, which, in turn, implies some sort of freely dynamic memory allocation.

But it doesn't explain what this looks like in practice, other than saying it is a "closure". This doesn't go into enough practical detail as well.

Basically, I would like to know how the call stack looks at different points in the example, such as in these steps:

  1. foo -> bar -> mul
  2. foo -> bar -> mul -> update
  3. foo -> bar -> console.log(x)

For example, foo -> bar -> mul means what does the function call stack and activation records look like at the step of say let m = x * a in the above code? etc..

I understand that you need to somehow keep a reference to the parent "scopes", but what does this look like in practice (say, if you were to show it in JSON or using JavaScript pseudocode)? And since the function bar is the return of foo, we are keeping foo's locals, but foo has returned. What does this do to the stack?

I would like to understand this because I am implementing a programming language. I would like this language to support all of the JavaScript features I outlined above, but I would like to implement it low-level like in a VM or using Assembly or the like, so need to know how the call stack functions in this advanced system.

来源:https://stackoverflow.com/questions/63763766/what-does-the-call-stack-look-like-in-a-system-of-nested-functions-functions-pa

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!