Вопрос о реализации вызовов функций в интерпретаторе

#interpreter

#интерпретатор

Вопрос:

Рассмотрим псевдокод:

 function test()
   print ('First Method')

test()
  

Когда интерпретатор считывает этот код, он вводит символ ‘test’ в таблицу символов, а затем компилирует функцию test () в байт-код

Когда дело доходит до test() , он сначала ищет test в таблице символов, находит его и выдает код, чтобы поместить этот символ в стек виртуальной машины. Затем он видит () и выдает байт-код ‘call’ . В итоге мы получаем следующую последовательность вызовов

 push test
call
  

Что произойдет, если у нас также будет второй тест, объявленный в тех же сценариях, например:

 function test()
   print ('First Method')

test()

function test()
   print ('Second Method')
  

Байтовый код:

 push test
call
push test <- but which test?
  

Когда интерпретатор обнаруживает вторую функцию, у нее то же имя, что и у первой функции, что мы делаем? Если мы заменим запись таблицы символов новой тестовой функцией, мы потеряем запись, на которую полагался вызов test() . Я заметил, что интерпретаторы, такие как R и Python, сохраняют вызов исходной тестовой функции.

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

 function test()
   print ('First Method')

test()

x = "This is in the first scope"

function test()
   print ('Second Method')

print (x)
  

Сначала print (x) будет искать символ x в локальной области, а затем во внешней области.

Комментарии:

1. В языках с такой семантикой имена функций являются изменяемыми переменными, а определения функций — присваиваниями. Таким образом, вы бы обработали переопределение так test же, как и переназначение любой другой переменной. Кстати, пользователь может также написать test = someOtherFunction или даже test = 42 и последующие (но не предыдущие) вызовы test() будут затронуты этим назначением (то есть в Python и R — хотите ли вы, чтобы ваш язык вел себя так же, конечно, зависит от вас).

2. Согласен, что имена функций действуют как любая другая переменная. Итак, если я переопределю тестовую переменную, объявив вторую функцию с тем же именем, когда байт-код выполняется и сталкивается с первым тестом, этот тест был переназначен второй функции. Но это не то поведение, которое я вижу в python или R.

3.Я не уверен, что вы имеете в виду — почему он должен был быть переназначен второй функции, если вы вызываете test() перед указанным переназначением? При запуске x = 42; print(x); x = 23 на Python будет выведено 42, а не 23, верно? Аналогично, если вы пишете f = lambda: print(42); f(); f = lambda: print(23) , он все равно будет печатать 42. И если вы замените лямбда-присваивание на a def , оно все равно будет печатать 42, потому что, по сути, a def ничем не отличается от назначения лямбда-выражения (за исключением того, что вы не можете записать все как лямбда-выражение в Python).

4. Я посмотрел на байт-код python о том, как он обрабатывает функции, и я вижу, что python связывает объект функции с именем символа во время выполнения, используя код операции MAKE_FUNCTION .

5. «Однако, когда он собирает байтовый код, второй f = lambda: print (23) перезапишет запись таблицы символов для первого лямбда» Назначения не должны перезаписывать записи таблицы символов (если бы они это сделали, x=42; print(x); x=23 пример в конечном итоге напечатал бы 23), они должны переводиться в хранилища, которыевыполняется во время выполнения. Таблица символов просто сообщает нам, где присваивание должно хранить присвоенное значение (и, возможно, другую статическую информацию, такую как типы, в случае статически типизированных языков).