Внедрение закрытий в Lua?

У меня вопрос о том, как закрываются блокировки.

Скажите, что это файл с именем test.lua:

local a = 'asdf'

local function b()
    return a
end

a = 10

return b

И еще один файл

a = require 'test'
a()

он напечатает

10

Если a является указателем на стеке на 'asdf' (в куче я предполагаю, но это не имеет значения), а замыкание b создается так, что предположительно адрес, который был в a сохраняется для b для использования, как a = 10 также меняет указатель внутри замыкания?

Википедия довольно хорошо говорит о том, что озадачивает меня:

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

Я думал, что, возможно, b действительно не сохранил указатель на 'asdf', а смещение стека на a, так что вы можете изменить a, и смещение стека приведет вас к a который указывает на последнее, что вы установили a, но как это работает, когда a (указатель) выскользнул из стека, и смещение стека становится недействительным?

1 Я знаю, что Lua не выделяет значения в стеке, но он выделяет локальные указатели в стеке значениям в куче, не так ли?

Ответ 1

Я действительно хочу, чтобы вы назвали эти переменные немного более разумно. Поэтому я буду:

local inner = 'asdf'

local function b()
    return inner
end

inner = 10

return b

и

func = require 'test'
func()

ОК, теперь, когда мы знаем, о чем говорим, я могу продолжить.

Lua chunk test имеет локальную переменную с именем inner. Внутри этого фрагмента вы создаете новую функцию b. Так как это новая функция, она имеет область действия в пределах фрагмента test.

Поскольку он находится внутри функции, он имеет право доступа к локальным переменным, объявленным вне этой функции. Но поскольку он находится внутри функции, он не получает доступа к таким переменным, как к одному из своих локальных. Компилятор обнаруживает, что inner - это локальная переменная, объявленная за пределами области действия, поэтому она преобразует ее в то, что Lua называет "upvalue".

Функции в Lua могут иметь произвольное количество значений (до 255), связанных с ними, называемое "upvalues". Функции, созданные на C/С++, могут хранить некоторое количество upvalues, используя lua_pushcclosure. Функции, созданные компилятором Lua, используют upvalues ​​для обеспечения лексического охвата.

Область - это все, что происходит в фиксированном блоке кода Lua. Итак:

if(...) then
  --yes
else
  --no
end

Блок yes имеет область видимости, а блок no имеет разную область. Любые переменные local, объявленные в блоке yes, не могут быть доступны из блока no, потому что они находятся за пределами области no.

Конструкции Lua, которые определяют область действия, это if/then/else/end, while/do/end, repeat/until, do/end, for/end и function/end. Кроме того, каждый script, называемый "куском" Lua, имеет область действия.

Вложенные объекты. В пределах одной области действия вы можете получить доступ к локальным переменным, объявленным в более высокой области.

"Стек" представляет все переменные, объявленные как local в пределах определенной области. Поэтому, если у вас нет локальных переменных в определенной области, стек для этой области пуст.

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

В Lua все по-другому. Стек для конкретной области видимости - это объект, а не только указатель. Для любой конкретной области действия существует определенное количество переменных local. Когда интерпретатор Lua входит в область действия, он "распределяет" стек размера, необходимого для доступа к этим локальным переменным. Все ссылки на локальные переменные являются просто смещениями в этот стек. Доступ к локальным переменным из более высоких областей (ранее определенных) просто обеспечивает доступ к другому стеку.

Итак, в Lua у вас концептуально есть стек стеков (который я буду называть "s-стеком" для ясности). Каждая область создает новый стек и толкает его, и когда вы покидаете область действия, она выталкивает стек из s-стека.

Когда компилятор Lua встречает ссылку на переменную local, она преобразует эту ссылку в индекс в s-стек и смещение в этот конкретный стек. Поэтому, если он обращается к переменной в текущем локальном стеке, индекс в s-стек относится к вершине s-стека, а смещение - это смещение в этот стек, где находится переменная.

Это прекрасно для большинства конструкций Lua, которые имеют доступ к областям. Но function/end не просто создают новую область; они создают новую функцию. И этой функции разрешен доступ к стекам, которые не только локальный стек этой функции.

Стеки - это объекты. И в Lua объекты подлежат сборке мусора. Когда интерпретатор входит в область действия, он выделяет объект стека и толкает его. Пока объект стека выталкивается в s-стек, он не может быть уничтожен. Стек стеков относится к объекту. Однако, как только интерпретатор выходит из области действия, он выталкивает стек из s-стека. Так как он больше не ссылается, он подлежит сбору.

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

Итак, пока объект функции продолжает существовать, так же, как и стек (ы), который он ссылается.

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

Обратите внимание, что это может быть не так, как это реализовано, но что общая идея позади.