?

Log in

No account? Create an account

Метапрограммирование в Агде и немного философии - Excelsior

Aug. 27th, 2013

09:49 am - Метапрограммирование в Агде и немного философии

Previous Entry Share Next Entry

Comments:

[User Picture]
From:xeno_by
Date:August 29th, 2013 09:25 am (UTC)
(Link)
Ну и плюс, тут есть Скало-специфичная особенность. От лексического контекста зависит implicit resolution, а его заранее точно не посчитаешь, т.к. он работает на типах.
(Reply) (Parent) (Thread)
From:Valentin Budaev
Date:August 29th, 2013 10:07 am (UTC)
(Link)
Эээ... а как оно зависит от _стороннего_ лексического контекста?
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:August 29th, 2013 03:54 pm (UTC)
(Link)
object A {
  class HasX { def x = 42 }
  implicit def anythingHasX(anything: Any): HasX = new HasX
  def helper(arg: Tree) = q"$arg.x"
}

object B {
  macro m = A.helper(q"new Object")
}

m
Вызов макроса m раскроется в new Object().x, и хотелось бы, чтобы последующая компиляция экспаншена нашла anythingHasX, объявленный в лексическом скоупе квазицитаты.

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

Пока что самым простым решением кажется сериализация всего исходного файла вместе с позицией квазицитаты, а потом on-demand тайпчек new Object().foo в контексте сериализованного файла в нужной позиции. Тут походу тоже можно использовать марки для определения того, в каком именно контексте тайпчекать.
(Reply) (Parent) (Thread)
From:Valentin Budaev
Date:August 29th, 2013 04:41 pm (UTC)
(Link)
> Вызов макроса m раскроется в new Object().x, и хотелось бы, чтобы последующая компиляция экспаншена нашла anythingHasX

Я понял. Тут мешается два концепта - модули (единицы, эквивалентные модулям в вашем языке) и лексические контексты. Сразу уточним отличие лексического контекста от модуля, чтобы не было путаницы - обращение к переменной лексического контекста извне контекста невозможно, а вот обращение к переменной модуля извне модуля - возможно.

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

Как я понимаю, семантически проблема решается следующим образом - все модули (в вашем случае - companion-object'ы, что еще может выступать в их качестве?), которые есть в текущей точке (доступны в данном лексическом контексте), объединены в один такой большой общий контекст-неймспейс (соответственно, и все имплициты, грубо говоря внутренность всех этих модулей "сплайсится" во внешний лексический контекст, который эти модули содержит), при этом часть переменных приватна и мы не можем их использовать в обычном коде, но можем - если в них раскрывается макрос.

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

Когда мы в вашем случае используем m, то имеется транзитивная зависимость от модуля A (m тянет B, B тянет А), соответственно, он должен быть как-то подгружен и весь его внутренний контекст должен быть приватно-доступен в точке вызова m. Тогда единственное, что нам нужно - это привязка цитаты к модулю (сорцев уже и не требуется). А сам модуль и так у нас должен быть. То есть получается такой "локальный импорт" - для выдернутой из B квазицитаты локально импортируется внутренность В.

С фазами это все легко и просто получается, но у вас jvm под капотом, так что тут не знаю. Вообще я как-то плохо понимаю именно механизм работы скальных макросов (как что раскрывается и т.п. - именно на уровне рантайма), потому тут мало что скажу, не кинете, кстати, ссылку на paper по этой теме (как вообще макросы в скале работают)?

ЗЫ: а собственно, можно ведь просто для квазицитаты, которую мы выдернули, создать локальный контекст, поместить ее внутрь и в этом контексте добавить импорт:
m раскроется в
{
import A._

new Object().x
}

только это должно делаться неявно и импортируется модуль, который указан в source квазицитаты

ЗЫЫ: а можно даже в точке объявления квазицитаты ее сразу преобразовывать к приведенному выше виду, тогда даже не надо хранить в цитате ссылку на исходный модуль

Edited at 2013-08-29 05:05 pm (UTC)
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:August 30th, 2013 10:38 pm (UTC)
(Link)
Прошу прощения за поздний ответ - сегодня утром появилось срочное дело, которое заняло весь день.

Макросы в Скале работают никак. Есть папера про применение макросов: http://scalamacros.org/paperstalks/2013-04-22-LetOurPowersCombine.pdf, но устранение адхочности и подпиливание макросистемы там отмечено как future work, которым я собственно сейчас и занимаюсь.

Идея про импорты и замечание про то, что окружающий модуль != лексический контекст, звучат очень хорошо. Я, правда, не уверен, что все сложится как надо, т.к.: 1) implicit scope включает в себя не только lexical scope, но еще и экзотические вещи вроде содержимого текущего package object, 2) может как-то выстрелить наследование. Надо внимательно посмотреть, чтобы это все не порушилось.
(Reply) (Parent) (Thread)
From:Valentin Budaev
Date:August 31st, 2013 01:10 am (UTC)
(Link)
> implicit scope включает в себя не только lexical scope, но еще и экзотические вещи вроде содержимого текущего package object

Это вы о том, что должен даваться доступ не только к содержимому класса, в котором объявлен имплицит, но и к содержимому пакета, в котором этот класс определен (и который, в общем случае, отличается от пакета, в котором вызван макрос)? Ну по идее это решается добавлением еще одного импорта (для самого пакета).

> может как-то выстрелить наследование.

Это да, выстрелить много чего может - потому и нужна семантика макроэкспанда, чтобы все формально строго проследить.
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:August 31st, 2013 06:56 am (UTC)
(Link)
О, я понял еще две проблемы: 1) куча импортов может породить конфликты, когда одно и то же имя импортируется несколькими импортами, 2) у вещей, живущих в implicit scope, есть приоритеты, которые стираются, когда мы импортируем все сразу. Но подумать нужно, да.
(Reply) (Parent) (Thread)
From:Valentin Budaev
Date:August 31st, 2013 11:28 pm (UTC)
(Link)
> 1) куча импортов может породить конфликты, когда одно и то же имя импортируется несколькими импортами

Вы забыли учесть гигиену. У того, что внутри {}, из-за нее нету доступа к контексту, в котором находится {}, то есть к импортам точки макровызова. Ну а если в точке макроопределения неоднозначность - то ее и надо решать в точке определения, во время написания квазицитаты, путем указания полного квалификатора, например.

Другое дело, если вы хотите, чтобы могли подхватываться имплициты одновременно точки макроопределения и точки макровызова, но тут уже какая-то неконсистентность выходит.
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:September 1st, 2013 06:32 am (UTC)
(Link)
Нет, подхватывать оба класса имплиситов я бы не хотел. Это действительно неконсистентно.

Про импорты я имел ввиду несколько другое. В Скале и у имплиситов, и у импортов есть приоритеты. Например, если я напишу "import A._; { import B._; ... }", то внутри многоточий импорты из B будут более приоритетны, чем импорты из A. Соответственно, есть шанс, что явное задание импортов сломает приоритеты. Впрочем, наверное, можно исхитриться с вложенностью, чтобы эмулировать нужные приоритеты, но тогда программисту, читающему принтауты квазицитат, может стать неудобно.
(Reply) (Parent) (Thread)
From:Valentin Budaev
Date:September 1st, 2013 09:54 pm (UTC)
(Link)
> Например, если я напишу "import A._; { import B._; ... }"

Всмысле в точке макроопределения?

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

Я не имел ввиду, что надо _реально_ трансформировать квазицитаты в {} с импортами, я говорил о семантике (чтобы можно было reasoning вести), а реализовать можно уже руками в недрах макросистемы. Это же вполне обычный способ - описываем фичу де-юре как некий сахар над ядром, а реализуем уже более эффективно/удобно, главное, чтобы семантически реализация была эквивалентна сахару.

То есть если мы можем обернуть квазицитату в {} с импортами, нам ведь в принципе ничего не мешает воспользоваться апи компилятора и "руками" указать ему: "считай, что здесь были вот эти импорты, хотя их и нет по факту". Или мешает?

ЗЫ: а source location в любом случае для отладки тянуть полезно в представлении АСТ, без него нормальные ошибки из макроса не прокинешь.
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:September 2nd, 2013 11:59 am (UTC)
(Link)
Понятно. Да, можно оставить такие вещи деталью реализации. Думаю, что компилятору это можно объяснить.

Позишены (так у нас называются source locations) тянуть уже можно прямо сейчас, так что по крайней мере здесь у нас все в порядке :)
(Reply) (Parent) (Thread)
From:Valentin Budaev
Date:September 3rd, 2013 01:33 am (UTC)
(Link)
> Позишены (так у нас называются source locations) тянуть уже можно прямо сейчас

Так это же замечательно! :)
Кстати, сразу - очень неплохо было бы, чтобы стандартным поведением макроса был перенос positions с АСТ, которое макрос получил на входе, на АСТ, которое макрос дает на выходе. В Racket, например, не так, и по-хорошему надо обрамлять 90% макросов в syntax/loc, при этом в макроформах типа define-simple-macro/define-syntax-rule этого сделать нельзя (т.к. нельзя написать код первой фазы) и они получаются по умолчанию немного "поломанными".

Еще, кстати, пара вопросов:
1. Как я понимаю, macro-generated macro в скале сделать не получится?

2. Экспанд происходит во время тайпчека?

3. Такой пример:

object Macro {
  def fun = ...
  def macroImpl(c: Context)(...) = ...
}


здесь fun можно использовать как в коде самого макроса, так и внутри цитаты? То есть аналога разделению на фаз нету, стейт общий, как в CL, например?

4. Я так понял, у вас сразу в макросе происходит деконструкция АСТ на аргументы, например:

def assertImpl(c: Context)
    (cond: c.Expr[Boolean], msg: c.Expr[Any]) : c.Expr[Unit] =


Есть ли способы обратиться к исходному, не разобранному на аргументы (cond, expr) АСТ? Если нет - теряется потенциально полезная информация.

Edited at 2013-09-03 01:39 am (UTC)
(Reply) (Parent) (Thread)
[User Picture]
From:xeno_by
Date:September 3rd, 2013 07:48 am (UTC)
(Link)
0. По умолчанию макрос не трогает позиции деревьев, которые приходят ему на вход. Синтетические деревья, которым позиция не сопоставлена, получают позицию макро вызова, который сейчас раскрывается.

1. Пока что нет, но есть планы. Не знаю, как это делается в Схеме, но я планирую это сделать через интерпретацию деревьев. Раньше меня останавливало то, что такая интерпретация неизбежно будет довольно убогой, но опыт D меня вдохновил. CTFE вначале действительно был совсем слабенький, но потом они наворачивали все больше и больше и сейчас там уже поддерживается здоровенный кусок языка.

2. В основном, да. Макро аннотации, которые могут добавлять/удалять мемберы, раскрываются до тайпчека.

3. Разделения фаз нету потому, что раскрытие макросов и исполнение кода, получившегося в результате раскрытия, сейчас не могут происходить в одном и том же джава рантайме. Когда я начну делать интерпретацию, ситуация сразу изменится и придется думать, что делать.

4. Да, способ есть. c.macroApplication.

Вообще, наше обсуждение это часть большого редизайна нашей макросистемы, которым я сейчас занимаюсь. Если вам будет интересно, я расшарю первую версию дизайн-документа, когда она будет готова для прочтения кем-нибудь кроме меня (сейчас там еще во многих местах полнейший хаос).
(Reply) (Parent) (Thread)
From:Valentin Budaev
Date:September 3rd, 2013 08:59 am (UTC)
(Link)
> Синтетические деревья, которым позиция не сопоставлена, получают позицию макро вызова, который сейчас раскрывается.

Ага, это хорошо.

> Не знаю, как это делается в Схеме

В схеме оно just works. Экспандер раскрывает форму -> смотрит не является ли оно define-syntax, если является - вычисляет в +1 фазе тело и добавляет в лексический контекст информацию о том, что с соответствующим идентификатором связан макространсформер. И дальше при раскрытии если этот сгенеренный макрос встретится, то он уже будет раскрываться, т.к. добавлен в контекст.

> я планирую это сделать через интерпретацию деревьев.

Это как? В смысле, что вообще под этим подразумевается?

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

О, мне очень интересно будет :)
(Reply) (Parent) (Thread) (Expand)
[User Picture]
From:xeno_by
Date:September 2nd, 2013 11:59 am (UTC)
(Link)
Про импорты, да. Например, в точке макроопределения.
(Reply) (Parent) (Thread)