(сказаное ниже относится к версии 2.018.3)
Это 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 - версия компонента (должна быть выше чем может быть указано в зависимостях от этого компонента).
Кроме того, реализация позволяет использовать дополнительные ключи (для каких-нибудь собственных мета-надстроек), чуть позже список этих дополнительных ключей будет связан с лексической переменной other-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)
(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 :
- для типа :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, то осуществляется проверка синтаксической корректности заданной версии. Это должна быть строка, содержащая числа, разделённые точками:
5. Дополнительные ключи связываются с лексической переменной other-args:
(let* ((other-args (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 #'find-system) weakly-depends-on)))В этом коде происходит присоединение к depends-on тех систем которые получилось найти. Принцип такой: не нашли, значит обойдёмся. С какой стати "систем", ведь функция parse-component-form вызывается (как мы увидим позже) вообще для всех элементов системы? Очевидно ключ :weakly-depends-on имеет право быть только в форме верхнего уровня (по отношению к форме (defsystem ...). Если его указать для какого-то вложенного компонента, то логично предположить что будут подгружаться системы соответствующие именам в этом списке, что врятли соответствует ожиданиям разработчика. Видимо авторам следовало бы либо изменить поиск систем на поиск компонентов/файлов либо ввести проверку на отсутствия ключа :weakly-depends-on в описании вложенных компонентов.
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
(apply 'reinitialize-instance ret
:name (coerce-name name)
:pathname pathname
:parent parent
other-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)
:name (coerce-name name)
:pathname pathname
:parent parent
other-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. Возврат созданного компонента.
-----------------------------
Продолжение
следует ...