linkfly (linkfly) wrote,
linkfly
linkfly

Category:

Устройство 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. Возврат созданного компонента.
-----------------------------

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

Tags: asdf, lisp, programming, лисп, программирование
Subscribe

  • Post a new comment

    Error

    default userpic

    Your reply will be screened

    When you submit the form an invisible reCAPTCHA check will be performed.
    You must follow the Privacy Policy and Google Terms of use.
  • 10 comments