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:
- 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.
- 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.
- 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?