Почему CLISP не может вызывать определенные функции с неинтернированными именами?

Я написал специальный генератор парсера, который создает код для преобразования старого и малоизвестного 7-битного набора символов в юникод. Вызов генератора парсера расширяется до группы defun, заключенных в progn, которые затем компилируются. Я хочу открыть только один из сгенерированных defuns — верхний уровень — для остальной части системы; все остальные являются внутренними для синтаксического анализатора и вызываются только из динамической области действия анализатора верхнего уровня. Следовательно, другие сгенерированные defun имеют неинтернированные имена (созданные с помощью gensym). Эта стратегия отлично работает с SBCL, но недавно я впервые протестировал ее с CLISP и получил такие ошибки, как:

*** - FUNCALL: undefined function #:G16985

Кажется, что CLISP не может обрабатывать функции с неинтернированными именами. (Интересно, что система скомпилирована без проблем.) EDIT: кажется, что в большинстве случаев она может обрабатывать функции с неинтернированными именами. Смотрите ответ Рёрда ниже.

Мои вопросы: это проблема с CLISP или это ограничение Common Lisp, которое некоторые реализации (например, SBCL) преодолевают?

ИЗМЕНИТЬ:

Например, расширение макроса сгенерированной функции верхнего уровня (называемой parse) имеет такое выражение:

(PRINC (#:G75735 #:G75731 #:G75733 #:G75734) #:G75732)

Вычисление этого выражения (путем вызова parse) вызывает ошибку, подобную приведенной выше, даже несмотря на то, что функция определенно определена в том же раскрытии макроса:

(DEFUN #:G75735 (#:G75742 #:G75743 #:G75744) (DECLARE (OPTIMIZE (DEBUG 2)))
 (DECLARE (LEXER #:G75742) (CONS #:G75743 #:G75744))
 (MULTIPLE-VALUE-BIND (#:G75745 #:G75746) (POP-TOKEN #:G75742)
 ...

Два экземпляра #:G75735 определенно являются одним и тем же символом, а не двумя разными символами с одним и тем же именем. Как я уже сказал, это работает с SBCL, но не с CLISP.

ИЗМЕНИТЬ:

Пользователь SO Джошуа Тейлор указал, что это связано с давней ошибкой CLISP.


person nbtrap    schedule 03.10.2013    source источник
comment
Обозначения в печатном изображении те же. Однако, когда вы пишете что-то вроде #:foo, читатель создает новый символ, поэтому, например, вводя свой REPL, вы получаете (eq '#:foo '#:foo) ;=> nil. Вы пытались вызвать функцию, введя ее имя?   -  person Joshua Taylor    schedule 03.10.2013
comment
@JoshuaTaylor Как я уже сказал в посте, символы — это один и тот же символ. Макрос вызывает gensym только один раз и помещает результат в несколько мест расширения. Конечно, чтение расширения макроса приведет к созданию двух разных символов, но это не то, что происходит.   -  person nbtrap    schedule 03.10.2013
comment
Можете ли вы создать некоторый прямой код, который воспроизводит эту проблему. Я могу написать макрос, определяющий некоторые взаимно рекурсивные функции, именуемые неинтернированными символами, и он отлично работает в CLISP. См., например, pastebin.com/7MNGkgta . У него есть две функции с неинтернированными именами и одна с именем, которое упрощает вызов, и, кажется, работает нормально.   -  person Joshua Taylor    schedule 03.10.2013
comment
@JoshuaTaylor Я действительно не могу этого сделать, не упростив это до такой степени, что, насколько мне известно, это может начать работать. Если вы хотите написать мне по электронной почте (nbtrap AT nbtrap DOT com), я могу выслать вам фактический код проекта, который, как вы увидите, отлично работает с SBCL, но не с CLISP. Проблема в том, что макрос, генерирующий синтаксический анализатор, слишком сложен, чтобы его можно было упростить до примера, хотя код, который он генерирует, довольно прост (набор огромных case выражений).   -  person nbtrap    schedule 03.10.2013
comment
Ну, это идея, верно? Доберитесь до точки, где это работает, сделайте один шаг назад и спросите, в чем разница. Это будет вопрос, на который вы почти наверняка получите исчерпывающий ответ. Еще одна вещь, которую я бы предложил для отладки, — это использование необязательного строкового аргумента для gensym, чтобы получить более читаемые макрорасширения. Это не должно повлиять на эту проблему, но может упростить ее отслеживание.   -  person Joshua Taylor    schedule 03.10.2013
comment
@JoshuaTaylor Это на самом деле не объясняет, почему он работает с SBCL, а не с CLISP. Как я уже сказал, макрорасширение довольно простое — оно просто огромно. И CLISP прекрасно компилирует код.   -  person nbtrap    schedule 03.10.2013
comment
Если с кодом все в порядке, то неудивительно, что он работает в SBCL. Если это ошибка CLISP, вам следует найти место, где она начинает возникать. Если это довольно простое расширение макроса, то не должно быть слишком сложно придумать более простой пример, который воспроизводит проблему, верно?   -  person Joshua Taylor    schedule 03.10.2013
comment
@JoshuaTaylor Я имел в виду, что код, сгенерированный макросом, прост и аналогичен примерам REPL, которые здесь приводили другие (которые работают). Пожалуйста, посмотрите второе редактирование ответа Рорда и комментарии под ним - я думаю, что он что-то задумал.   -  person nbtrap    schedule 03.10.2013
comment
@JoshuaTaylor Хотя ты прав. Рано или поздно мне придется привести простой пример. Особенно, если мне нужно подать отчет об ошибке.   -  person nbtrap    schedule 03.10.2013
comment
О, это могло быть найдено при дополнительном гуглении: взгляните на этот отчет об ошибке #180 uninterned символы, не используемые между формами в файле FAS. Я нашел ссылку на него в этом обсуждении списка рассылки. Это относится к вашему фактическому коду? Также см., возможно, #281 Лексическое связывание в PROGN верхнего уровня (Clisp 2.35).   -  person Joshua Taylor    schedule 03.10.2013
comment
Да, похоже, это баг. Небеса, это старо. Думаю, мне придется просто использовать обходной путь. Ну что ж.   -  person nbtrap    schedule 03.10.2013


Ответы (5)


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

Если бы вы имели в виду сам символ, все, что вам нужно было бы сделать при реализации lisp, — это найти symbol-function этого символа. Интернирован он или нет, не имеет значения.

Могу я спросить, почему вы не рассмотрели другой способ скрыть функции, то есть оператор labels или определение функций в новом пакете, который экспортирует только одну внешнюю функцию?

EDIT: следующий пример буквально скопирован из взаимодействия с подсказкой CLISP.

Как видите, вызов функции, названной gensym, работает, как и ожидалось.

[1]> (defmacro test ()
(let ((name (gensym)))
`(progn
(defun ,name () (format t "Hello!"))
(,name))))
TEST
[2]> (test)
Hello!
NIL

Может быть, ваш код, который пытается вызвать функцию, оценивается до defun? Если в расширении макроса есть какой-либо код, кроме различных defun, это может зависеть от реализации, что будет оцениваться первым, и поэтому поведение SBCL и CLISP может отличаться, и ни один из них не нарушает стандарт.

РЕДАКТИРОВАНИЕ 2. Некоторые дальнейшие исследования показывают, что поведение CLISP зависит от того, интерпретируется ли код напрямую или сначала компилируется, а затем интерпретируется. Вы можете увидеть разницу, либо непосредственно load создавая файл Lisp в CLISP, либо сначала вызывая compile-file для него, а затем load создавая FASL.

Вы можете увидеть, что происходит, взглянув на первый перезапуск, который предлагает CLISP. Он говорит что-то вроде «Введите значение, которое будет использоваться вместо (FDEFINITION '#: G3219)». Таким образом, для скомпилированного кода CLISP заключает символ в кавычки и обращается к нему по имени.

Однако кажется, что это поведение соответствует стандарту. Следующее определение можно найти в HyperSpec:

обозначение функции n. обозначение функции; то есть объект, который обозначает функцию и является одним из: символа (обозначающего функцию, названную этим символом в глобальной среде) или функции (обозначающей саму себя). Последствия не определены, если символ используется в качестве обозначения функции, но не имеет глобального определения в качестве функции или имеет глобальное определение в виде макроса или специальной формы. См. также обозначение расширенной функции.

Я думаю, что неинтернированный символ соответствует случаю «символ используется в качестве обозначения функции, но не имеет глобального определения в качестве функции» для неуказанных последствий.

EDIT 3: (Я могу согласиться с тем, что я не уверен, является ли поведение CLISP ошибкой или нет. Об этом должен судить кто-то, более опытный в деталях терминологии стандарта. Все сводится к тому, является ли функциональная ячейка неинтернированного символа — т. е. символа, на который нельзя ссылаться по имени, только имея прямое удержание объекта символа — будет считаться «глобальным определением» или нет)

Во всяком случае, вот пример решения, которое решает проблему в CLISP путем интернирования символов в одноразовый пакет, избегая вопроса о неинтернированных символах:

(defmacro test ()
  (let* ((pkg (make-package (gensym)))
         (name (intern (symbol-name (gensym)) pkg)))
    `(progn
       (defun ,name () (format t "Hello!"))
       (,name))))

(test)

EDIT 4: Как отмечает Джошуа Тейлор в комментарии к вопросу, похоже, это случай (10-летнего) ошибка CLISP №180.

Я проверил оба обходных пути, предложенных в этом отчете об ошибке, и обнаружил, что замена progn на locally на самом деле не помогает, но замена на let () помогает.

person Rörd    schedule 03.10.2013
comment
Хороший вопрос. Причина, по которой я не использую labels или встроенные функции (что я и делал изначально), заключается в том, что единственная сгенерированная функция в таком случае настолько велика (> 30 000 строк), что компилятору не хватает памяти и он не может скомпилировать Это. - person nbtrap; 03.10.2013
comment
См. Мое редактирование исходного вопроса для примера. - person nbtrap; 03.10.2013
comment
Спасибо за предложение, но defun определенно в правильном порядке. Вызовы происходят после определений. - person nbtrap; 03.10.2013
comment
Одно отличие между приведенным вами примером и тем, с чем я имею дело, заключается в том, что я не вызываю функцию с неинтернированным именем напрямую. Вместо этого я вызываю функцию верхнего уровня, которая, в свою очередь, вызывает одну или несколько функций с неинтернированными именами. На вашем примере я протестировал более аналогичный случай из REPL, и он все равно работал. Хм... - person nbtrap; 03.10.2013
comment
Что касается вашего второго редактирования: я не думаю, что оно соответствует. Стандарт определяет семантику языка, но вы сравниваете его с какой-то промежуточной формой, в которую CLISP компилирует язык. Дело в том, что символы, обрабатываемые оценщиком для defun и вызова(ов) к нему, являются одними и теми же символами. Этот факт, кажется, теряется в той форме, в которую CLISP компилирует код. - person nbtrap; 03.10.2013
comment
Я согласен с @nbtrap по этому поводу. Немного о символах, имеющих глобальное определение, не имеет ничего общего с тем, интернированы они или нет. Это означает, что на локальные функции (т. е. определенные с помощью flet и label) нельзя ссылаться с помощью символов. Вы не можете выполнять (flet ((foo () ...)) (funcall 'foo)) и использовать символ foo для обозначения функции, определенной с помощью flet. - person Joshua Taylor; 03.10.2013
comment
@JoshuaTaylor Я посмотрю подробнее и сообщу об ошибке, если понадобится. - person nbtrap; 03.10.2013
comment
@Rörd, предложение использовать одноразовый пакет - хорошая идея. Спасибо. - person nbtrap; 03.10.2013
comment
@Rörd Глядя на компиляцию, это был хороший трек. Я нашел несколько отчетов об ошибках и упомянул их в этот комментарий к основному вопросу, но, возможно (поскольку у вас есть принятый ответ), вы можете добавить ссылки в свой ответ. - person Joshua Taylor; 03.10.2013
comment
@JoshuaTaylor: я добавил ссылку на основной отчет об ошибке в свой ответ. - person Rörd; 04.10.2013

Вы наверняка можете определить функции, имена которых являются неинтернированными символами. Например:

CL-USER> (defun #:foo (x)
           (list x))
#:FOO
CL-USER> (defparameter *name-of-function* *)
*NAME-OF-FUNCTION*
CL-USER> *name-of-function*
#:FOO
CL-USER> (funcall *name-of-function* 3)
(3)

Однако синтаксис резкое двоеточие вводит новый символ каждый раз, когда такая форма читается читается:

#: вводит неинтернированный символ с именем имя-символа. Каждый раз, когда встречается этот синтаксис, создается отдельный неинтернированный символ. Имя символа должно иметь синтаксис символа без префикса пакета.

Это означает, что даже если что-то вроде

CL-USER> (list '#:foo '#:foo)
;=> (#:FOO #:FOO) 

показывает одно и то же печатное представление, на самом деле у вас есть два разных символа, как показано ниже:

CL-USER> (eq '#:foo '#:foo)
NIL

Это означает, что если вы попытаетесь вызвать такую ​​функцию, набрав #:, а затем имя символа, именующего функцию, у вас возникнут проблемы:

CL-USER> (#:foo 3)
; undefined function #:foo error

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

(defun #1=#:fact (n &optional (acc 1))
  (if (zerop n) acc
      (#1# (1- n) (* acc n))))

используя специальные обозначения для чтения #1=#:fact и #1#, чтобы позже ссылаться на тот же символ. Однако посмотрите, что происходит, когда вы печатаете ту же форму:

CL-USER> (pprint '(defun #1=#:fact (n &optional (acc 1))
                    (if (zerop n) acc
                        (#1# (1- n) (* acc n)))))

(DEFUN #:FACT (N &OPTIONAL (ACC 1))
  (IF (ZEROP N)
      ACC
      (#:FACT (1- N) (* ACC N))))

Если вы возьмете этот распечатанный вывод и попытаетесь скопировать и вставить его как определение, читатель создаст два символа с именем «ФАКТ», когда дело доходит до двух вхождений #:FACT, и функция не будет работать (и вы даже можете получить предупреждения о неопределенных функциях):

CL-USER> (DEFUN #:FACT (N &OPTIONAL (ACC 1))
           (IF (ZEROP N)
               ACC
               (#:FACT (1- N) (* ACC N))))

; in: DEFUN #:FACT
;     (#:FACT (1- N) (* ACC N))
; 
; caught STYLE-WARNING:
;   undefined function: #:FACT
; 
; compilation unit finished
;   Undefined function:
;     #:FACT
;   caught 1 STYLE-WARNING condition
person Joshua Taylor    schedule 03.10.2013
comment
В своем первоначальном вопросе я попытался дать понять, что понял это и что проблема не в этом. - person nbtrap; 03.10.2013
comment
Я видел, что два экземпляра #:G75735 определенно являются одним и тем же символом, а не двумя разными символами с одним и тем же именем. но мне было непонятно, как вы пытались вызвать функции. Извините за путаницу. Однако код, который я опубликовал с функциями с неинтернированными именами, работает в CLISP. Можете ли вы придумать минимальный рабочий пример, который мы можем попробовать и в CLISP? - person Joshua Taylor; 03.10.2013
comment
Да, возможно, я недостаточно ясно выразился. Код, который я привожу в исходном посте, взят из расширения макроса — читатель никогда его не увидит. - person nbtrap; 03.10.2013

Надеюсь, я правильно понял задачу. Для меня это работает в CLISP.

Я пробовал так: используя макрос для создания функции с именем GENSYM-ed.

(defmacro test ()  
  (let ((name (gensym)))  
    `(progn  
       (defun ,name (x) (* x x))  
       ',name)))

Теперь я могу получить имя (setf x (test)) и назвать его (funcall x 2).

person Frank Zalkow    schedule 03.10.2013
comment
Оказывается, это давняя ошибка в CLISP. Похоже, что если бы вы вместо ',name в качестве последней формы поставили, например, (,name 45), и поместили (test) в файл, а затем скомпилировали и загрузили этот файл, вы могли бы воспроизвести ошибку. - person Joshua Taylor; 04.10.2013
comment
@JoshuaTaylor На самом деле это работает. Если я заменю ',name на (,name 45) и сохраню его в test.lisp, я смогу вызвать (load (compile-file "test.lisp")), а (test) даст 2025. Я использую GNU CLISP 2.49 под Mac OS X 10.5.8. - person Frank Zalkow; 05.10.2013
comment
Я тоже не проверял, понятно. Может быть, если бы вместо прямого вызова вы определили какую-то другую функцию, которая его вызывала? И пробовал вызывать это после компиляции и загрузки? Отчет об ошибке делает вид, что не требуется слишком много изменений, чтобы сломать что-то. :) - person Joshua Taylor; 05.10.2013
comment
Да, вот пример, который показывает ошибку pastebin.com/ux40R8uE . Просто поместите его в файл, скомпилируйте и загрузите, а затем вызовите (frob). - person Joshua Taylor; 05.10.2013
comment
@JoshuaTaylor Мне это не кажется ошибкой. Функция (frob) не определена, потому что имя (defun ,frob () 42) расширено до #:FROB792 или что-то в этом роде. Но я могу позвонить (call-frob) и получить 42. - person Frank Zalkow; 05.10.2013
comment
Извините, я неправильно написал. Не звоните (frob), звоните (call-frob), который должен вызывать псевдо-frob, и который имеет ошибку. Я написал слишком быстро. Однако код воспроизводит ошибку. У меня просто ошибка в описании. :) Вот вставка с более подробными инструкциями: pastebin.com/pBTUJr5B . - person Joshua Taylor; 05.10.2013

Да, совершенно нормально определять функции, имена которых являются непреднамеренными символами. Проблема в том, что вы не можете затем называть их "по имени", так как вы не можете получить неинтернированный символ по имени (по сути, это и означает "неинтернированный").

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

person Vatine    schedule 03.10.2013
comment
Я не пытаюсь называть их по имени. Я вызываю другую функцию по имени, которая, в свою очередь, вызывает функции с неинтернированными именами. - person nbtrap; 03.10.2013

Удивительно, но ошибка CLISP 180 на самом деле не является ошибкой соответствия ANSI CL. Не только это, очевидно, что ANSI Common Lisp сам по себе настолько сломан в этом отношении, что даже обходной путь, основанный на progn, является любезностью реализации.

Common Lisp — это язык, предназначенный для компиляции, и компиляция вызывает проблемы с идентификацией объектов, которые помещаются в скомпилированные файлы и загружаются позже («внешние» объекты). ANSI Common Lisp требует, чтобы литеральные объекты, воспроизведенные из скомпилированных файлов, были только похожи на исходные объекты. (CLHS 3.2.4 Буквальные объекты в скомпилированных файлах).

Во-первых, в соответствии с определением подобия (3.2.4.2.2 Определение подобия), правило для неинтернированных символов заключается в том, что сходство основано на имени. Если мы скомпилируем код с литералом, который содержит неинтернированный символ, то при загрузке скомпилированного файла мы получим похожий символ, а не (обязательно) тот же объект: символ с тем же именем.

Что, если один и тот же неинтернированный символ будет вставлен в две разные формы верхнего уровня, которые затем будут скомпилированы в виде файла? Когда файл загружается, эти два хотя бы похожи друг на друга? Нет, такого требования нет.

Но становится еще хуже: также не требуется, чтобы два экземпляра одного и того же неинтернированного символа в одной и той же форме были экстернализированы таким образом, чтобы сохранялась их относительная идентичность: чтобы повторно загруженная версия этот объект будет иметь один и тот же объект символа во всех местах, где был оригинал. На самом деле определение сходства не содержит положений о сохранении круговой структуры и совместного использования подструктур. Если у нас есть такой литерал, как '#1=(a b . #1#), как литерал в скомпилированном файле, кажется, не требуется, чтобы он был воспроизведен как круговой объект с той же структурой графа, что и оригинал (изоморфизм графа). Правило подобия для минусов задается как наивная рекурсия: два минуса подобны, если их соответствующие car и cdr подобны. (Правило не может быть выполнено даже для круглых объектов, оно не завершается).

Вышеприведенное работает из-за реализаций, выходящих за рамки того, что требуется в спецификации; они предоставляют расширение, соответствующее (3.2.4.3 Расширения правил подобия ).

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

(defmacro foo (arg)
   (let ((g (gensym))
         (literal '(blah ,g ,g ,arg)))
      ...))

(defun bar ()
  (foo 42))

Функция bar содержит литерал с двумя вставками gensym, которые, согласно правилам подобия для cons и символов, не должны воспроизводиться как список, содержащий два вхождения одного и того же объекта на второй и третьей позициях.

Если приведенное выше работает должным образом, это связано с «расширениями правил сходства».

Таким образом, ответ на вопрос "Почему CLISP не может..." заключается в том, что, хотя CLISP предоставляет расширение для подобия, которое сохраняет структуру графа литеральных форм, оно не делает этого во всем скомпилированном файле, а только в пределах отдельных элементы верхнего уровня в этом файле. (Он использует *print-circle* для выделения отдельных элементов.) Ошибка в том, что CLISP не соответствует наилучшему возможному поведению, которое могут себе представить пользователи, или, по крайней мере, лучшему поведению, демонстрируемому другими реализациями.

person Kaz    schedule 08.12.2015