Table of Contents

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

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

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

Overview

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

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

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

<source> (: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")))))) </source>

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

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

1. Разбор параметров

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

<code>type = :module</code> <code>name = "exp-system"</code>

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

Ключи-параметры

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

Задающие содержимое, путь, и класс компонента по умолчанию:

<code>:components</code> - компоненты, содержащиеся в данном (например файлы исходников или другие модули).

<code>:pathname</code> - переопределённый путь для компонента.

<code>:default-component-class</code> - класс, которорый будет использоваться при задании типа <code>:file</code>

Задающие зависимости:

<code>:weakly-depends-on</code> - зависимости загружаются только в случае, если удалось их найти.

<code>:depends-on</code> - зависимости обязательные к загрузке.

Управляющие порядком операций:

<code>:serial</code> - каждый описанный компонент, становится автоматически зависимым от предыдущего компонента.

<code>:in-order-to</code> - этой опцией можно переопределить порядок применения операций к компонентам.

<code>:do-first</code> - недокументированный ключ, также служит для тонкой настройки, порядка применения операций.

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

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

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

2. Проверка того, что опции weakly-depends-on depends-on components in-order-to заданы правильными значениями (списками).

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

<source> (check-component-input type name weakly-depends-on depends-on components in-order-to) </source>

… значения type и name передаются лишь для формировании сообщения об ошибке. Проверка не сложная:

  • все проверяемые элементы должны быть списком - это раз (пусть даже и пустым).
  • если in-order-to не пустой список, первый его элемент должен быть тоже списком - это два.

3. Проверка на отсутствие или существование компонента только того-же типа на этом же уровне иерархии.

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

<source> (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)) </source>

В первом вызове 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.

<source> (defun* class-for-type (parent type) …) </source>

CLASS-FOR-TYPE работает следующим образом:

  • пытаемся найти класс представленный символом type, сначала в пакете символа, затем в текущем пакете и наконец в пакете :asdf :

<source> (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) </source>

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

<source> (and (eq type :file) (or (module-default-component-class parent) (find-class default-component-class))) </source>

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

4. Проверка на правильное задание ключа :version

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

<source> (when versionp (unless (parse-version version nil) (warn … ))) </source>

5. Получение дополнительных ключей.

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

<source> (let* ((args (list* :name (coerce-name name)

:pathname pathname

:parent parent (remove-keys (remove-keys '(components pathname … ) rest))) …) …) </source>

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

6. Попытка найти старый компонент

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

<source> (let* (… (ret (find-component parent name))) …) </source>

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

7. Модифицирование зависимостей depends-on, в соотвии со слабыми зависимостями, задаваемыми ключом weakly-depends-on.

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

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

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

8. Добавление зависимости от предыдущего компонента, если необходимо (задана опция :serial t).

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

<source> (when serial-depends-on (push serial-depends-on depends-on)) </source>

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

9. Создание или переинициализация компонента

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

9.1 Если компонент найден - переинициализация

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

<source> (if ret ; preserve identity (apply 'reinitialize-instance ret args) …) </source>

9.2 Если компонент не найден - создание

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

<source> (if ret (…) (setf ret (apply 'make-instance (class-for-type parent type) args))) </source>

10. Вычисление слота absolute-pathname

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

11. Получение компонента по умолчанию, создание компонентов, инициализация слота components-by-name

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

11.1 Вычисление слота default-component-class

Как видно из кода он либо берётся из ключа :default-component-class либо из соответствующего слота своего предка.

<source> (setf (module-default-component-class ret) (or default-component-class (and (typep parent 'module) (module-default-component-class parent)))) </source>

11.2 Создание компонентов на основе значения опции :components

Затем, на основе списков в значении ключа :components создаётся список с объектами созданными из этих списков и присваивается слоту 'components:

<source> (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)))) </source>

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

<source> (when serial-depends-on (push serial-depends-on depends-on)) </source>

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

Инициализация слота components-by-name для быстрого поиска компонентов.

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

<source> (compute-module-components-by-name ret) </source>

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

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

12. Установка слота load-dependencies скорректированным значением depends-on

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

<source> (setf (component-load-dependencies ret) depends-on) </source>

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

<source> (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)) </source>

13. Cлот in-order-to

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

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

<source> `((compile-op (compile-op ,@depends-on)) (load-op (load-op ,@depends-on))) </source>

14. Слот do-first

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

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

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

<source> `((compile-op (load-op ,@depends-on))) </source>

15. Обновление Inline-методов

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

<source> (%refresh-component-inline-methods ret rest) </source>

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

15.1 Удаление инлайн-методов в ф-ии %remove-component-inline-methods.

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

<source> (%remove-component-inline-methods component) </source>

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

15.2 Определение инлайн-методов в ф-ии %define-component-inline-methods.

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

<source> (%define-component-inline-methods component rest) </source>

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

<source> (dolist (name asdf-methods) (let ((keyword (intern (symbol-name name) :keyword))) …)) </source>

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

<source> (loop :for data = rest :then (cddr data) …) </source>

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

<source> (:PERFORM-WITH-RESTARTS :PERFORM :EXPLAIN :OUTPUT-FILES :OPERATION-DONE-P) </source>

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

<source> (eval `(defmethod ,name ,qual ((,o ,op) (,c (eql ,ret))) ,@body)) </source>

Это было неожидано, кстати. И потом, как можно догадаться, он кладётся в список слота 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. Возврат созданного компонента.