Каков порядок операций построения лексических окружений из функций и замыканий?

Это уточнение/доработка Как выглядит стек вызовов в системе вложенных функций, функций, передаваемых в качестве аргументов, и возвращаемых функций?

Вот некоторый вымышленный код, который демонстрирует 2 модуля и некоторые вложенные вызовы функций.

// 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
    }
  }
}

Итак, в основном есть 2 модуля, каждый из которых использует глобальные переменные в своих функциях. Второй модуль имеет ряд вложенных функций и возвращает функции в некоторых точках.

То, что я пытаюсь сделать, это собрать воедино порядок операций, который будет выполнять компилятор. Это то, что у меня есть до сих пор после многих часов:

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)

То есть каждая функция инициализируется (заворачивается) в замыкание.

Затем вы начинаете вызывать функции:

-> 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
                })

Или сплющенный, как это было бы на самом деле:

-> 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
    })

Итак, в основном:

  1. замыкание создается для каждой функции (перед вызовом любой функции). Он создает в основном список всех переменных, к которым вы можете получить доступ в функции.
  2. Вызов каждой функции создает новую среду, которая создается на основе замыкания. Это извлекает все переменные, определенные в замыкании, для использования в функции.
  3. Среда устанавливает все свои переменные в текущие значения. Или, может быть, у него есть указатели на значения или параметры родительской среды?

Я все еще не вижу полной картины. Если бы кто-нибудь мог написать псевдокод для порядка операций, это было бы все, что мне нужно. По сути, как ряд вложенных функций (и функций, определенных в других модулях с другими глобальными переменными) на самом деле реализуется как последовательность примитивных вызовов (для создания сред или чего-то еще).

В основном я пытаюсь обернуть голову вокруг реализации компилятора, который поддерживает эти функции. Мне нужно взять определения моих функций (как AST) и скомпилировать их в примитивные блоки инструкций, подобные сборке. Каждый блок инструкций или метка представляет собой последовательность инструкций (согласно определению сборки). Таким образом, это будет что-то вроде этого псевдокода сборки:

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

НО это будет включать гораздо больше, оно будет включать среды для каждого вызова. Больше похоже на это:

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

В общем, я застрял на этапе того, как представить преобразование вложенных функций и модулей, ориентированных на синтаксис языка, в последовательность инструкций, подобную ассемблеру. Особенно в том, что касается создания сред и замыканий.

В псевдокоде, не более элегантном, чем мой, какой должна быть последовательность инструкций для создания этих сред в структуре, похожей на Ассемблер?


person Lance Pollard    schedule 06.09.2020    source источник
comment
Какие закрытия вы хотите создать? Javascript var-closes или Javascript let-замыкания? Питон или Луа? Лисп или Схема? (По-видимому, ваше закрытие переживает область, в которой они созданы, поэтому мы можем, по крайней мере, исключить Pascal и C++ [&] lambdas.) Трудно ответить на вопрос в целом, не зная семантики, которую вы надеетесь реализовать. (Среды в стиле JS применимы только к некоторым реализациям закрытия.)   -  person rici    schedule 09.09.2020
comment
Если этот комментарий непонятен, рассмотрите разницу между function f(n) { a = Array(); for (var i = 0; i<n; ++i) a.push(()=>i); return a; } и очень похожим function g(n) { a = Array(); for (let i = 0; i<n; ++i) a.push(()=>i); return a; }; разница только в var и let. Но закрытия очень разные. Сравните f(3)[0]() и g(3)[0](). Итак, какой из двух Python? Попробуйте f=lambda n:[lambda:i for i in range(n)].   -  person rici    schedule 09.09.2020