Как вы компилируете макросы в компиляторе Лиспа?

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

Но в компиляторе нет функций, которые можно было бы вызывать для создания расширенного кода: Проблема довольно просто видна в следующем примере :

(defmacro cube (n)
    (let ((x (gensym)))
      `(let ((,x ,n))
          (* ,x ,x ,x))))

Когда макрос раскрывается интерпретатором, он вызывает gensym и делает то, что вы ожидаете. При расширении компилятором вы сгенерируете код для let , который связывает x с (gensym) , но символ gensymmed необходим только для компилятор поступил правильно. А поскольку gensym на самом деле не вызывается до компиляции макроса, он не очень полезен.

Это становится еще более странным для меня, когда макрос создает список, который будет использоваться в качестве расширения с использованием карта или фильтр .

Итак, как это работает? Конечно, скомпилированный код не компилируется в (eval * macro-code *) , потому что это было бы ужасно неэффективно. Есть ли хорошо написанный компилятор Lisp, в котором это ясно?

21
задан Rainer Joswig 1 January 2016 в 14:56
поделиться

2 ответа

Вы нашли одно из главных отличий между Лиспом и другими языками.

В Лиспе выполнение динамически создаваемого кода необходимо, и, например, необходимо для расширения макроса.

При написании lisp для C-компилятора я обнаружил эту теперь очевидную вещь сам и пришел к выводу, что если вы хотите написать Lisp-компилятор, есть только два решения:

  1. Вы напишите ОБА компилятор и интерпретатор, чтобы вы могли вызывать интерпретатор для раскрытия макроса во время компиляции.

  2. Вы должны быть в состоянии динамически компилировать код и вызывать его (или использовать худшие «хитрости», такие как компиляция динамически загружаемого модуля и его загрузка).

Если вы работаете над компилятором для C, одной из возможностей является использование библиотеки TCC Фабриса Белларда , которая позволяет напрямую компилировать код C в буфер памяти.

Я пишу Lisp для компилятора Javascript, и в этом случае, конечно, нет проблем, потому что «железо» может с этим справиться, и вы можете попросить Javascript, например, оценить строку "function(...){...}", а затем вызвать полученный результат. объект. Использование Javascript также решает, что является IMO одной из самых сложных проблем для ядра Lisp, которая заключается в правильной реализации лексических замыканий.

Действительно, в моем javascript-компиляторе eval просто более или менее

(defun eval (x)
    (funcall (js-eval (js-compile x))))

, где js-compile - основной интерфейс компилятора, и при наличии формы lisp вернет строку, содержащую код javascript, который при оценке (с eval JavaScript, который я экспортировал на уровень lisp как js-eval), выполняет код. Интересно также, что eval никогда не используется (за единственным несущественным исключением из вспомогательного макроса, в котором я должен выполнять пользовательский код во время расширения макроса).

Одна важная вещь, которую следует учитывать, заключается в том, что, хотя Common Lisp имеет своего рода разделение между «временем чтения», «временем компиляции» и «временем выполнения», все же это разделение является более логичным, чем физическим, поскольку работающий код всегда является Lisp. Компиляция в Лиспе - это просто вызов функции. Даже фаза "синтаксического анализа" - это просто выполнение обычной функции lisp ... это Лисп до конца: -)

Ссылки на мой компилятор игрушек Lisp → Js

4
ответ дан 29 November 2019 в 20:53
поделиться

Есть много подходов к этому. Одной из крайностей является то, что называется «FEXPER», и это макроподобные вещи, которые по существу пересматриваются при каждой оценке. Они вызывали много шума в прошлом, но почти полностью исчезли. (Есть несколько людей, которые до сих пор делают подобные вещи, хотя, вероятно, newlisp является наиболее популярным примером.)

Таким образом, FEXPER были выброшены в пользу макросов, которые в некотором смысле более «хорошо себя ведут». Вы в основном выполняете расширение макроса один раз и компилируете полученный код. Как обычно, здесь есть несколько стратегий, которые могут привести к разным результатам. Например, «развернуть один раз» не указывает , когда расширяется. Это может произойти, как только код будет прочитан, или (обычно), когда он скомпилирован, или даже только при первом запуске.

Другой вопрос здесь - и это, по сути, то, где вы стоите - заключается в том, в какой среде вы оцениваете код макроса. В большинстве Лиспов все происходит в одной и той же счастливой глобальной среде. Макрос может свободно обращаться к функциям, что может привести к некоторым тонким проблемам. Одним из результатов этого является то, что многие коммерческие реализации Common Lisp предоставляют вам среду разработки, в которой вы выполняете большую часть своей работы и компилируете вещи - это делает одну и ту же среду доступной на обоих уровнях. (На самом деле, поскольку макросы могут использовать макросы, здесь существует произвольное количество уровней.) Для развертывания приложения вы получаете ограниченную среду, в которой нет, например, компилятора (то есть функции compile), поскольку если вы развертываете код, который использует это, ваш код по сути является компилятором CL. Таким образом, идея состоит в том, что вы компилируете код на полную реализацию, и это расширяет все макросы, что означает, что скомпилированный код не использует макросы дополнительно.

1114 Но, конечно, это может привести к тем тонким проблемам, о которых я говорил. Например, некоторые побочные эффекты могут привести к беспорядку в порядке загрузки, когда вам нужно загрузить код в определенном порядке. Хуже того, вы можете попасть в ловушку, где код выполняется для вас в одну сторону, а в другую - когда компилируется - поскольку во скомпилированном коде все макросы (и вызовы, которые они совершали) уже были расширены заранее. Есть несколько хакерских решений, таких как eval-when , которые определяют определенные условия для оценки некоторого кода. Есть также несколько систем пакетов для CL, где вы указываете такие вещи, как порядок загрузки (например, asdf ). Тем не менее, там нет реального надежного решения, и вы все равно можете попасть в эти ловушки (см., Например, этот расширенный спор ).

1115 Конечно, есть альтернативы. В частности, Racket использует свою модульную систему. Модуль может быть "создан" несколько раз, и состояние уникально для каждого экземпляра. Теперь, когда какой-либо модуль используется как в макросах, так и во время выполнения, два экземпляра этих модулей различны, что означает, что компиляция всегда надежна, и ни одна из перечисленных выше проблем не возникает. В мире Scheme это называется «отдельными фазами», где каждая фаза (время выполнения, время компиляции и более высокий уровень с макросами-использованием-макросами) имеет отдельные экземпляры модуля. Для хорошего введения в это и подробного объяснения, прочитайте Matthew Flatt Composable and Compilable Macros . Вы также можете просто взглянуть на Racket docs , например, раздел Compile и Run-Time Phases .

6
ответ дан 29 November 2019 в 20:53
поделиться
Другие вопросы по тегам:

Похожие вопросы: