0

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

Here is some madeup code that demonstrates 2 modules and some nested function calling.

// module_1.js

// global variables
var CONST = 6541

function add(a, b) {
  return CONST + a + b
}

function sub(a, b) {
  return CONST - a - b
}

function mul(a, b) {
  return CONST * a * b
}

// module_2.js

// global variables
var a = 11
var b = 5
var c = 2

main()

function main() {
  var v = foo_1()
  var w = foo_2()

  var t = v(1, 2, 3)
  var u = w(4)
  var k = add(t, u)

  // OUTPUT
  console.log(k)

  // nested functions
  function foo_1() {
    var d = bar_1(2, 3)
    var e = bar_2(10)

    // returning functions as results.
    return bar_3

    // super nested functions.
    function bar_1(x, y) {
      var m = mul(x, a)
      var n = sub(m, y)
      var o = mul(n, c)
      return o
    }

    function bar_2(x) {
      var m = mul(x, x)
      var n = add(m, 3)
      var o = mul(n, a)
      return n
    }

    function bar_3(x, y, z) {
      var m = mul(x, a)
      var n = sub(m, y)
      var o = add(n, b)
      var p = mul(o, z)
      var q = sub(p, c)
      var r = mul(q, d)
      var s = add(r, e)
      return s
    }
  }

  function foo_2() {
    return bar_2

    function bar_1(x, y) {
      var p = add(y, a)
      var q = add(x, p)
      return q
    }

    function bar_2(x) {
      var p = bar_1(x, 2)
      var q = bar_1(x, 4)
      var r = mul(p, a)
      var s = add(q, c)
      return r * s
    }
  }
}

So basically, there are 2 modules, each with global variables used in their functions. The second module has a series of nested functions and returns functions at some points.

What I'm trying to do is piece together the order of operations a compiler would do. This is what I have so far after many hours:

main.closure = createClosure(a, b, c)
main.foo_1.closure = createClosure(parent: main.closure, d, e)
main.foo_1.bar_1.closure = createClosure(parent: main.foo_1.closure, x, y, m, n, o)
mul.closure = createClosure(CONST, a, b)

That is, each function is initialized with (wrapped in) a closure.

Then you start calling the functions:

-> call(main)
  -> main.env = createEnvironment({
      closure: main.closure,
      a: 11, 
      b: 5, 
      c: 2
    })
    -> call(foo_1)
      -> main.foo_1.env = createEnvironment({
          closure: main.foo_1.closure,
          // with parent environment
          parent: main.env, 
          d: null, 
          e: null
        })
        -> call(bar_1, 2, 3)
          -> main.foo_1.bar_1.env = createEnvironment({
              closure: main.foo_1.bar_1.closure,
              // with parent environment
              parent: main.foo_1.env, 
              x: 2, 
              y: 3, 
              m: null, 
              n: null, 
              o: null
            })
            -> call(mul, main.foo_1.bar_1.env.x, main.foo_1.bar_1.env.parent.parent.a)
              -> mul.env = createEnvironment({
                  closure: mul.closure,
                  // no parent environment
                  CONST: 6541,
                  a: main.foo_1.bar_1.env.x,
                  b: main.foo_1.bar_1.env.y
                })

Or flattened out as it would actually be:

-> call(main) =
  -> main.env = createEnvironment({
      closure: main.closure,
      a: 11, 
      b: 5, 
      c: 2
    })
-> call(foo_1) =
  -> main.foo_1.env = createEnvironment({
      closure: main.foo_1.closure,
      // with parent environment
      parent: main.env, 
      d: null, 
      e: null
    })
-> call(bar_1, 2, 3) =
  -> main.foo_1.bar_1.env = createEnvironment({
      closure: main.foo_1.bar_1.closure,
      // with parent environment
      parent: main.foo_1.env, 
      x: 2, 
      y: 3, 
      m: null, 
      n: null, 
      o: null
    })
-> call(mul, main.foo_1.bar_1.env.x, main.foo_1.bar_1.env.parent.parent.a) =
  -> mul.env = createEnvironment({
      closure: mul.closure,
      // no parent environment
      CONST: 6541,
      a: main.foo_1.bar_1.env.x,
      b: main.foo_1.bar_1.env.y
    })

So basically:

  1. A closure is created for every function (before any function is called). It creates basically a list of all the variables you can access in the function.
  2. The invocation of every function creates a new environment, which is created based on the closure. This fetches all the variables defined in the closure, for use in the function.
  3. The environment sets all its variables to the current values. Or maybe it has pointers to the parent environment values or parameters?

I am still not quite seeing the full picture. If anyone could write out the pseudocode for the order of operations that would be all I'm looking for. Basically, how a series of nested functions (and functions called defined in other modules with other "globals") is actually implemented as a sequence of primitive calls (to creating environments, or whatever may be).

Basically I am trying to wrap my head around implementing a compiler that supports these features. I need to take my function definitions (as an AST), and compile it down to primitive assembly-like instruction blocks. Each instruction "block" or "label" is a sequence of instructions (as per assembly definition). So it would be something like this assembly pseudocode:

data a, 11
data a, 5
data a, 2

main:
  call foo_1
  call foo_2
  ...
foo_1:
  push 2
  push 3
  call bar_1
  ...
foo_1_bar_1:
  ...
...
  ...
foo_2:
  ...
foo_3:
  ...

BUT, it would include way more than this, it would include the environments for each call. More like this:

main:
  // create main environment
  push a
  push b
  push c
  // some other stuff perhaps for constructing the environment.
  call foo_1
  ...

So basically, I am stuck in the step of how to imagine the conversion of the language-syntax-oriented "nested functions and modules", to the assembly-like "sequence of instructions". Especially as it relates to creating environments and closures.

In pseudocode no more elegant than mine necessarily, what would the sequence of instructions be for creating these environments in an Assembly-like sort of structure?

Lance
  • 75,200
  • 93
  • 289
  • 503
  • 1
    What sort of closures do you want to create? Javascript var-closures or Javascript let-closures? Python or Lua? Lisp or Scheme? (Apparently your closure outlive the scope in which they're created, so we can at least eliminate Pascal and C++ [&] lambdas.) It's hard to answer the question in general without knowing the semantics you are hoping to implement. (JS-style "environments" only apply to some closure implementations.) – rici Sep 09 '20 at 17:26
  • In case that comment wasn't clear, consider the difference between `function f(n) { a = Array(); for (var i = 0; ii); return a; }` and the very similar `function g(n) { a = Array(); for (let i = 0; ii); return a; }`; the only difference is `var` and `let`. But the closures are very different. Contrast `f(3)[0]()` and `g(3)[0]()`. Now, which of the two is Python? Try `f=lambda n:[lambda:i for i in range(n)]`. – rici Sep 09 '20 at 18:09

0 Answers0