November 1st, 2011

Устройство ASDF. Подсистема определения. PARSE-COMPONENT-FORM.

UPDATED 11.12.2011. Разработчиками были удалены декораторы над ф-ией REINITIALIZE-INSTANCE. А также упрощён код связанный с повторной инициализацией или созданием объекта класса/подкласса COMPONENT.

UPDATED 14.12.2011. Скорректирован код обрабатывающий ключ :weakly-depends-on. Упомянуто отображение в документации того, что ключ :weakly-depends-on имеет смысл для верхнего уровня определения системы.

(сказаное ниже относится к версии 2.019.5)

Это 4-ая статья цикла.
Первая статья.
Архитектура ASDF.
Предыдущая статья.

    Функция PARSE-COMPONENT-FORM вызывается из ф-ии do-defsystem и представляет собой следующий и главный этап определения системы. Ф-ия строит иерархию объектов (класса component и его наследников) на основе передаваемых опций и присоединяет её к другому объекту, её определение выглядит так:

    (defun* parse-component-form (parent options) ...)

В parent передаётся объект к которому нужно присоединить создаваемую иерархию, в options передаются ключи управляющие созданием объектов. Если parse-component-form вызывается из do-defsystem, parent будет равен nil (это означает, что будет создаваться корневой объект иерархии). Список options выглядит подобно следующему:

    (:module "exp-system"
      :pathname #P"/home/someuser/lisp/asdf-experiments/"
      :depends-on nil
      :components ((:module "src"
                                  :pathname ""
                                  :components ((:file "file1")
                                                            (:static-file "static.txt")
                                                            (:file "file2" :depends-on ("file1"))
                                                            (:file "file3" :depends-on ("file1"))))))

    ... это те же опции, что используются в форме (defsystem ...) в *.asd файлах, но за исключением опции :class (так как, если она была задана, её обработка произошла до вызова parse-component-form в ф-ии do-defsystem).

            Логика работы parse-component-form.

1. Ф-ия с помощью destructuring-bind разбирает переданные параметры и устанавливает локальные переменные соответствующие их ключам. Есть правда, небольшое исключение: первые два элемента считаются обязательными (а не ключевыми) и локальными переменными для них будут type и name. Для примера выше (при разборе options) установки этих переменных будут следующие:

type = :module
name = "exp-system"

    Остальные имена локальных переменных будут соответствовать переданным ключам. Ключи :perform :explain :output-files :operation-done-p используются для создания инлайн-методов (inline methods) специализирующихся на этом компоненте, но их обработка происходит вне определения parse-component-form (конкретно в ф-ии %define-component-inline-methods вызываемой из %refresh-component-inline-methods, которая в свою очередь вызывается в конце вызова parse-component-form) и поэтому они помечены как ignorable (игнорируемые) чтобы подавить ненужные предупреждения. Вообще список возможных инлайн-методов содержится в константе +asdf-methods+. Список содержит символы именующие методы, соответственно упомянутым ключам (а также символ perform-with-restarts, соответствующий недокументированному инлайн-методу). Итак остаются следующие ключи:

    Задающие содержимое, путь, и класс компонента по умолчанию:
:components
:pathname
:default-component-class

    Задающие зависимости:
:weakly-depends-on
:depends-on

    Управляющие порядком операций:
:serial
:in-order-to
:do-first

    Дополнительные:
:version

    Чтобы вы при чтении дальнейшего описания, примерно представляли о чём идёт речь (конечно же, для более обстоятельного объяснения стоит обратится к официальной документации) ниже дано короткое описание, назначения опций:

    Задающие содержимое, путь, и класс компонента по умолчанию:
:components - компоненты, содержащиеся в данном (например файлы исходников или другие модули).
:pathname - переопределённый путь для компонента.
:default-component-class - класс, которорый будет использоваться при задании типа :file

    Задающие зависимости:
:weakly-depends-on - зависимости загружаются только в случае, если удалось их найти.
:depends-on - зависимости обязательные к загрузке.

    Управляющие порядком операций:
:serial - каждый описанный компонент, становится автоматически зависимым от предыдущего компонента.
:in-order-to - этой опцией можно переопределить порядок применения операций к компонентам.
:do-first - недокументированный ключ, также служит для тонкой настройки, порядка применения операций.

    Дополнительные:
:version - версия компонента (должна быть выше чем может быть указано в зависимостях от этого компонента).

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

2. Далее parse-component-form вызывает ф-ию check-component-input для проверки значений, связанных с лексическими переменными weakly-depends-on, depends-on, components и in-order-to.

    (check-component-input type name weakly-depends-on depends-on components in-order-to)

    ... значения type и name передаются лишь для формировании сообщения об ошибке.  Проверка не сложная:
        - все проверяемые элементы должны быть списком - это раз (пусть даже и пустым).
        - если in-order-to не пустой список, первый его элемент должен быть тоже списком - это два.

3. Дальше идёт проверка того, что если определяемый компонент уже существует на том же уровне иерархии (а именно в компоненте parent), то он такого же типа, что и определяемый (иначе сигнализируется ошибка):

   (when (and parent
                        (find-component parent name)
                       ;; ignore the same object when rereading the defsystem
                        (not
                           (typep (find-component parent name)
                                       (class-for-type parent type)
)
)
)

      (error 'duplicate-names :name name)
)


    В первом вызове parse-component-form аргумент parent равен nil, поэтому проверка сразу пропускается. А вообще, суть проверки такова: если parent не nil и компонент найден в parent и тип компонента отличается от указанно типа, то имеет место коллизия имён и выбрасывается ошибка duplicate-names.
    Но почему здесь не сигнализируется ошибка, если был найден компонент того же типа и с тем же именем что и определяемый? Это было сделано для ситуации повторного чтения определения системы (например, если файл .asd изменился). Дело в том, что хэш-таблица в слоте components-by-name, объекта parent (который должен иметь тип/подтип module), используемая в методе find-component, будет содержать (при переопределении системы) старые записи компонентов. И конечно, найдется компонент с тем же именем, что и определяемый. Как видно, разработчики сделали так, чтобы сигнализация ошибки при изменении типа компонентов происходила пораньше. Непосредственно проверка того, что на том же уровне иерархии нет компонентов с одинаковым именем, осуществляется в ф-ии compute-module-components-by-name. Эта ф-ия выполняет итерацию по содержимому слота components (объекта класса/подкласса module) с тем, чтобы создать и заполнить хэш-таблицу с записями вида имя_компонента-компонент и записать её в слот components-by-name. а также сигнализировать ошибку duplicate-names, если встретились компоненты с одинаковым именем. Она будет вызвана здесь же, в parse-component-form, если определяемый компонент имеет тип/подтип module.
    В показаном выше коде, исопльзуется ф-ия class-for-type. Её определение достаточно тривиально, но имеет важный нюанс: используется слот default-component-class передаваемого объекта parent, а при равенство его NIL - динамическая переменная *default-component-class*.

    (defun* class-for-type (parent type) ...)
    CLASS-FOR-TYPE работает следующим образом:
        - пытаемся найти класс представленный символом type, сначала в пакете символа, затем в текущем пакете и наконец в пакете :asdf :

       (loop :for symbol :in (list
                                               type
                                              (find-symbol* type *package*)
                                              (find-symbol* type :asdf)
)

         :for class = (and symbol (find-class symbol nil))
         :when (and class (subtypep class 'component))
         :return class
)


        - для типа :file делается исключение, для него не обязательно иметь класс. При его использовании инстанцируемый класс выбирается следующим образом - если в слоте компонента default-component-class есть значение, то это будет возвращаемым значением, если нет, то значением будет класс *default-component-class*, который по умолчанию равен CL-SOURCE-FILE:

   (and (eq type :file)
             (or (module-default-component-class parent)
                   (find-class *default-component-class*)
)
)


Логика работы find-component здесь рассматриваться не будет, так как это тема для отдельной статьи.

4. Если была задана опция с ключом :version, то осуществляется проверка синтаксической корректности заданной версии. Это должна быть строка, содержащая числа, разделённые точками:

   (when versionp
      (unless (parse-version version nil)
        (warn ... )
)
)

5. Дополнительные ключи связываются с лексической переменной args:

   (let* ((args (list* :name (coerce-name name)
                        :pathname pathname
                        :parent parent
                        (remove-keys (remove-keys '(components pathname ... )
                                                             rest
)
)
)
               ...
)

      ...
)

 
    Эти ключи и их значения будут участвовать в создании (или повторной инициализации) компонента. А именно дополнительные аргументы передаются в make-instance (если компонент ещё не был создан) или в reinitialize-instance (если компонент был получен, после успешного поиска в parent), но об этом позже.

6. Лексической переменной ret присваивается компонент, если он уже был создан или конкретней: присваивается компонент с именем name содержащейся в parent:

   (let* (...
            (ret (find-component parent name))
)

      ...
)


    Если это первый вызов parse-component-form и соотв. аргумент parent равен nil, а аргумент name соответствует имени определяемой системы (оно сейчас содержится в переменной name и было передано через ключевой параметр :module) - вызов вернёт объект представляющий эту систему. Если же parent и name заданы (не равны nil), то производится поиск компонента в parent. Это нужно для того, чтобы заново не пересоздавать уже готовые объекты (а значит не выделять заново для них память, что важно).

7. Теперь обрабатывается ключик :weakly-depends-on - фактически это не что иное как список "не обязательных" систем:

   (when weakly-depends-on
      (appendf depends-on (remove-if (complement #'(lambda (x) (find-system x nil))) weakly-depends-on)))

В этом коде происходит присоединение к depends-on тех систем которые получилось найти. Принцип такой: не нашли, значит обойдёмся. С какой стати "систем", ведь функция parse-component-form вызывается (как мы увидим позже) вообще для всех элементов системы? Очевидно ключ :weakly-depends-on имеет право быть только в форме верхнего уровня (по отношению к форме (defsystem ...). Если его указать для какого-то вложенного компонента, то логично предположить что будут подгружаться системы соответствующие именам в этом списке, что врятли соответствует ожиданиям разработчика. Видимо авторам следовало бы либо изменить поиск систем на поиск компонентов/файлов либо ввести проверку на отсутствия ключа :weakly-depends-on в описании вложенных компонентов (впрочем, то что этот ключ имеет смысл для верхнего уровня определения системы - теперь, начиная с версии 2.019.5, отображено в документации).

8. Далее используется динамическая переменная *serial-depends-on* - если её содержимое не равно nil, это содержимое добавляется в depends-on:

   (when *serial-depends-on*
       (push *serial-depends-on* depends-on)
)


    По умолчанию *serial-depends-on* = nil, позже мы увидим в какой ситуации это будет не так. Вообще эта переменная работает совместно с ключом :serial - она содержит предыдущий, определёный в parse-component-form, компонент (на том же уровне иерархии) и как видно выше модифицирует список depends-on компонента включая туда этот компонент.

9. Далее, создаётся или переинициализируется объект класса/подкласса component:

    9.1 Если компонент был найден (при первом вызове, это понятное дело объект класса system или его наследника), то его необходимо повторно инициализировать, используя для этого, в том числе, дополнительные опции:

       (if ret ; preserve identity
         (apply 'reinitialize-instance ret args)
         ...)


    9.2 Если компонента в parent не было найдено - создаётся новый объект типа, имя которого связано с локальной type. Причём создаётся натурально из указанного типа, например если у вас в определении системы указан :module создаётся объект класса module. Для получения класса по type используется уже рассмотренная выше ф-ия class-for-type. То есть, совершенно свободно можете определять свои классы в иерархии наследования которых есть класс component и использовать в списках, внутри списка опции :components (исключение составляет, как показно выше в описании ф-ии class-for-type, ключ :file):

       (if ret
            (...)
         
(setf ret (apply 'make-instance (class-for-type parent type) args)))

10. Для компонента вычисляется значение слота absolute-pathname: (component-pathname ret). Принцип такой: по пути к самому старшему предку в иерархии, которым должна быть система, собираются именя компонентов и присоединяются к абсолютному пути этого корневого компонента, то есть системы. Для объекта-системы же, этот слот получает значение из слота relative-pathname, который должен быть абсолютным и вычисляется ещё в do-defsystem, а связывается со слотом во время повторной инициализации.

11. Далее, если компонент класса 'module (или его наследника) то выполняются следующие действия:

    11.1 Вычисляется слот 'default-component-class:

       (setf (module-default-component-class ret)
                (or default-component-class
                      (and (typep parent 'module)
                               (module-default-component-class parent)
)
)
)


        Как видно из кода он либо берётся из ключа :default-component-class либо из соответствующего слота своего предка.
        
    10.2 Затем, на основе списков в значении ключа :components создаётся список с объектами созданными из этих списков и присваивается слоту 'components:

           (let ((*serial-depends-on* nil))
               (setf (module-components ret)
                     (loop
                        :for c-form :in components
                        :for c = (parse-component-form ret c-form)
                        :for name = (component-name c)
                        :collect c
                        :when serial :do (setf *serial-depends-on* name)
)
)
)


        Обратите внимание, что создаётся локальный контекст в котором *serial-depends-on* приравнивается к nil, а каждый объект создаётся с помощью рекурсивного вызова всё той же parse-component-form (но уже в качестве parent выступает текущий объект). Здесь мы видим принцип работы ключа :serial - если он задан, то parse-component-form выполняется в контексте в котором *serial-depends-on* приравнена к предыдущему созданному компоненту, это влияет на форму (описанную в пункте 8):

        (when *serial-depends-on*
           (push *serial-depends-on* depends-on)
)


     ... то есть модифицирует значение depends-on, добавляя к нему имя предыдущего созданного компонента.

     11.3 Заполняется слот components-by-name создаваемой хэш-таблицей для быстрого поиска компонентов по имени:

       (compute-module-components-by-name ret)

     Там же осуществляется проверка на уникальность имён компонентов.

Дальнейшие действия происходят не только для объектов класса/подкласса module.

12. Далее устанавливается слот load-dependencies:

   (setf (component-load-dependencies ret) depends-on)

    ... в значение depends-on которое как мы помним могло быть модифицировано формами:

   (when weakly-depends-on
      (appendf depends-on (remove-if (complement #'find-system) weakly-depends-on))
)

   (when *serial-depends-on*
      (push *serial-depends-on* depends-on)
)


13. Теперь будет уставка слота in-order-to:

     (setf (component-in-order-to ret)
              (union-of-dependencies
                in-order-to
               `((compile-op (compile-op ,@depends-on))
                  (load-op (load-op ,@depends-on))
)
)
)


    Тело функции union-of-dependencies выглядит довольно хитро. Подробности её внутреннего устройство тема для отдельной статьи. Для начала следует иметь в виду, что она просто возвратит свой второй аргумент если опция :in-order-to не была установлена, а значит в этом случае слот in-order-to получит значение:

    `((compile-op (compile-op ,@depends-on))
       (load-op (load-op ,@depends-on)))  

14. Работа со слотом do-first происходит аналогичным образом:

    (setf (component-do-first ret)
             (union-of-dependencies
                do-first
                `((compile-op (load-op ,@depends-on)))))

    ... т.е. если опция :do-first не использовалась, то в слоте do-first сохраняется более ясное для понимания:

    `((compile-op (load-op ,@depends-on)))

15. Далее происходит следующее: обновляются, так называемые inline методы для компонента:

    (%refresh-component-inline-methods ret rest)

    При выполнении этой формы удаляются инлайн-методы компонента и определяются заново:

    15.1 Сначала удаляются все методы сохранённые в слоте inline-methods из обобщённых функций, сохранённых в
константе +asdf-methods+:

        (%remove-component-inline-methods component)

        Код этой функции достаточно тривиален и я не буду его здесь приводить.

    15.2 Затем слот inline-methods получает новый список методов используя для этого список оставшихся опций:

        (%define-component-inline-methods component rest)

        Код этой ф-ии тоже не сложный - для каждого символа в +asdf-methods+ создаётся соответствующий keyword:

        (dolist (name +asdf-methods+)
          (let ((keyword (intern (symbol-name name) :keyword)))
           ...
)
)


        Потом на каждой итерации происходит проход по списку опций компонента

         (loop :for data = rest :then (cddr data) ...)

        ...  и для каждого ключа из списка:

        (:PERFORM-WITH-RESTARTS :PERFORM :EXPLAIN :OUTPUT-FILES :OPERATION-DONE-P)

        ... генерируется и выполняется код создающий метод на основе значения ассоциированного с ключом:

       (eval `(defmethod ,name ,qual ((,o ,op) (,c (eql ,ret)))
                     ,@body
)
)


        Это было неожидано, кстати. И потом, как можно догадаться, он кладётся в список слота inline-methods.

16. Возвращение созданного компонента в качестве результата.

Для более ясной картины опишу вкратце все 16 действий, выполняемые parse-component-form:

1. Разбор ключевых параметров с помощью destructuring-bind.
2. Проверка того, что опции weakly-depends-on depends-on components in-order-to заданы правильными значениями (списками).
3. Проверка на отсутствие или существование компонента только того-же типа на этом же уровне иерархии.
4. Проверка на правильное задание ключа :version.
5. Получение дополнительных ключей.
6. Попытка найти старый компонент.
7. Модифицирование зависимостей depends-on, в соотвии со слабыми зависимостями, задаваемыми ключом weakly-depends-on.
8. Добавление зависимости от предыдущего компонента, если необходимо (задана опция :serial t).
9. Создание или переинициализация компонента:
      9.1 Если компонент найден при первом вызове, то - переинициализация.
      9.2 Если не был найден, то - создание.
10. Вычисление слота absolute-pathname.
11. Получение компонента по умолчанию, создание компонентов, инициализация слота components-by-name:
      11.1 Вычисление слота default-component-class по заданной опции или по слоту предка.
      11.2 Создание компонентов на основе значения опции :components.
      11.3 Инициализация слота components-by-name для быстрого поиска компонентов.
12. Установка слота load-dependencies скорректированным значением depends-on.
13. Установка слота in-order-to.
14. Установка слота do-first.
15. Обновление инлайн-методов.
      15.1 Удаление инлайн-методов в ф-ии %remove-component-inline-methods.
      15.2 Определение инлайн-методов в ф-ии %define-component-inline-methods.
16. Возврат созданного компонента.
-----------------------------

Продолжение следует ...