1

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.

Lance
  • 75,200
  • 93
  • 289
  • 503
  • It's not exactly trivial. Does https://en.wikipedia.org/wiki/Closure_(computer_programming)#Implementation_and_theory help? – Thomas Sep 06 '20 at 11:58
  • @Thomas it sheds a tiny bit of light, but still no specific examples of what the call stack looks like. That's the main thing I would like to see, what it looks like at specific points. – Lance Sep 06 '20 at 12:07
  • Perhaps this will help? https://craftinginterpreters.com/closures.html – Lance Sep 06 '20 at 12:14
  • I don't think we can go much more into detail than "*How do JavaScript closures work at a low level?*" The explanation there seems to be quite clear. – Bergi Sep 06 '20 at 16:26
  • Regarding you call stack examples, notice that `foo` never calls `bar`, so it's never on the call stack together with it. – Bergi Sep 06 '20 at 16:26
  • There is no way to draw with pseudocode/json how it looks? – Lance Sep 06 '20 at 16:59
  • 1
    If the pics in the linked question didn't help, maybe [this one](https://stackoverflow.com/q/37491626/1048572) (and possibly the ones I linked there in a comment) do? – Bergi Sep 06 '20 at 17:16
  • 1
    In Javascript, the environment records do not live on the stack. That's quite different from the Lua implementation, which is the basis for the Crafting Interpreters link. In Lua, the non-closed arguments live in the stack in a normal activation record, while the closed arguments are pointed to from the function closure arguments which require them. As long as the enclosed local's stack frame is still active, that's where the variable lives; when the frame is popped, any variables which have been enclosed are reallocated as boxes on the heap. Like JS, this depends on garbage collection. – rici Sep 06 '20 at 19:31

0 Answers0